mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-08 08:26:20 +00:00
Compare commits
No commits in common. "master" and "5.1.11" have entirely different histories.
2
.github/workflows/dotnet.yml
vendored
2
.github/workflows/dotnet.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 6.0.x
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -287,3 +287,5 @@ __pycache__/
|
|||||||
*.odx.cs
|
*.odx.cs
|
||||||
*.xsd.cs
|
*.xsd.cs
|
||||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||||
|
/Docs/*
|
||||||
|
Docs/
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -25,8 +24,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.That(result1);
|
Assert.True(result1);
|
||||||
Assert.That(result2);
|
Assert.True(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -40,8 +39,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.That(result1);
|
Assert.True(result1);
|
||||||
Assert.That(result2);
|
Assert.True(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -56,14 +55,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.That(result1);
|
Assert.True(result1);
|
||||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||||
|
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
|
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.That(result2);
|
Assert.True(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -76,13 +75,13 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.That(result1);
|
Assert.True(result1);
|
||||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
|
|
||||||
var result2 = await waiter2;
|
var result2 = await waiter2;
|
||||||
|
|
||||||
Assert.That(result2);
|
Assert.True(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -106,13 +105,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
for(var i = 1; i <= 10; i++)
|
for(var i = 1; i <= 10; i++)
|
||||||
{
|
{
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
await Task.Delay(1); // Wait for the continuation.
|
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await resultsWaiter;
|
await resultsWaiter;
|
||||||
|
|
||||||
Assert.That(10 == results.Count(r => r));
|
Assert.AreEqual(10, results.Count(r => r));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -126,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
Assert.That(result1);
|
Assert.True(result1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -138,7 +136,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var result1 = await waiter1;
|
var result1 = await waiter1;
|
||||||
|
|
||||||
ClassicAssert.False(result1);
|
Assert.False(result1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
@ -12,6 +11,66 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestFixture()]
|
[TestFixture()]
|
||||||
public class BaseClientTests
|
public class BaseClientTests
|
||||||
{
|
{
|
||||||
|
[TestCase]
|
||||||
|
public void SettingLogOutput_Should_RedirectLogOutput()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var logger = new TestStringLogger();
|
||||||
|
var client = new TestBaseClient(new BaseRestClientOptions()
|
||||||
|
{
|
||||||
|
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, false)]
|
||||||
|
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var logger = new TestStringLogger();
|
||||||
|
var options = new BaseRestClientOptions()
|
||||||
|
{
|
||||||
|
LogWriters = new List<ILogger> { logger }
|
||||||
|
};
|
||||||
|
if (verbosity != null)
|
||||||
|
options.LogLevel = verbosity.Value;
|
||||||
|
var client = new TestBaseClient(options);
|
||||||
|
|
||||||
|
// act
|
||||||
|
client.Log(testVerbosity, "Test");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||||
{
|
{
|
||||||
@ -19,10 +78,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestBaseClient();
|
var client = new TestBaseClient();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
var result = client.Deserialize<object>("{\"testProperty\": 123}");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result.Success);
|
Assert.IsTrue(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -32,11 +91,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestBaseClient();
|
var client = new TestBaseClient();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
var result = client.Deserialize<object>("{\"testProperty\": 123");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.IsTrue(result.Error != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[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")]
|
||||||
@ -49,7 +108,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||||
{
|
{
|
||||||
var result = baseUrl.AppendPath(path);
|
var result = baseUrl.AppendPath(path);
|
||||||
Assert.That(expected == result);
|
Assert.AreEqual(expected, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -18,9 +17,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult(new ServerError("TestError"));
|
var result = new CallResult(new ServerError("TestError"));
|
||||||
|
|
||||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
Assert.AreEqual(result.Error.Message, "TestError");
|
||||||
ClassicAssert.IsFalse(result);
|
Assert.IsFalse(result);
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -28,9 +27,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult(null);
|
var result = new CallResult(null);
|
||||||
|
|
||||||
ClassicAssert.IsNull(result.Error);
|
Assert.IsNull(result.Error);
|
||||||
Assert.That(result);
|
Assert.IsTrue(result);
|
||||||
Assert.That(result.Success);
|
Assert.IsTrue(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -38,10 +37,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult<object>(new ServerError("TestError"));
|
var result = new CallResult<object>(new ServerError("TestError"));
|
||||||
|
|
||||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
Assert.AreEqual(result.Error.Message, "TestError");
|
||||||
ClassicAssert.IsNull(result.Data);
|
Assert.IsNull(result.Data);
|
||||||
ClassicAssert.IsFalse(result);
|
Assert.IsFalse(result);
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -49,10 +48,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new CallResult<object>(new object());
|
var result = new CallResult<object>(new object());
|
||||||
|
|
||||||
ClassicAssert.IsNull(result.Error);
|
Assert.IsNull(result.Error);
|
||||||
ClassicAssert.IsNotNull(result.Data);
|
Assert.IsNotNull(result.Data);
|
||||||
Assert.That(result);
|
Assert.IsTrue(result);
|
||||||
Assert.That(result.Success);
|
Assert.IsTrue(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -61,11 +60,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|
||||||
ClassicAssert.IsNull(asResult.Error);
|
Assert.IsNull(asResult.Error);
|
||||||
ClassicAssert.IsNotNull(asResult.Data);
|
Assert.IsNotNull(asResult.Data);
|
||||||
Assert.That(asResult.Data is not null);
|
Assert.IsTrue(asResult.Data is TestObject2);
|
||||||
Assert.That(asResult);
|
Assert.IsTrue(asResult);
|
||||||
Assert.That(asResult.Success);
|
Assert.IsTrue(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -74,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||||
var asResult = result.As<TestObject2>(default);
|
var asResult = result.As<TestObject2>(default);
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
Assert.IsNotNull(asResult.Error);
|
||||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
|
Assert.AreEqual(asResult.Error.Message, "TestError");
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
Assert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
Assert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
Assert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -87,11 +86,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
Assert.IsNotNull(asResult.Error);
|
||||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
Assert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
Assert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
Assert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -100,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
Assert.IsNotNull(asResult.Error);
|
||||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
Assert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
Assert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
Assert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -112,29 +111,26 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
|
||||||
"{}",
|
"{}",
|
||||||
1,
|
|
||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
ResultDataSource.Server,
|
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
Assert.IsNotNull(asResult.Error);
|
||||||
Assert.That(asResult.Error.Message == "TestError2");
|
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
Assert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
Assert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
Assert.IsFalse(asResult.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -142,28 +138,25 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
|
||||||
"{}",
|
"{}",
|
||||||
1,
|
|
||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
ResultDataSource.Server,
|
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|
||||||
ClassicAssert.IsNull(asResult.Error);
|
Assert.IsNull(asResult.Error);
|
||||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||||
ClassicAssert.IsNotNull(asResult.Data);
|
Assert.IsNotNull(asResult.Data);
|
||||||
Assert.That(asResult);
|
Assert.IsTrue(asResult);
|
||||||
Assert.That(asResult.Success);
|
Assert.IsTrue(asResult.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
194
CryptoExchange.Net.UnitTests/ConverterTests.cs
Normal file
194
CryptoExchange.Net.UnitTests/ConverterTests.cs
Normal file
@ -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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
@ -18,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.ClampValue(min, max, input);
|
var result = ExchangeHelpers.ClampValue(min, max, input);
|
||||||
Assert.That(expected == result);
|
Assert.AreEqual(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
||||||
@ -35,7 +33,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||||
Assert.That(expected == result);
|
Assert.AreEqual(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
||||||
@ -50,7 +48,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
||||||
Assert.That(expected == result);
|
Assert.AreEqual(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(5, 0.1563158, 0.15631)]
|
[TestCase(5, 0.1563158, 0.15631)]
|
||||||
@ -61,7 +59,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
||||||
Assert.That(expected == result);
|
Assert.AreEqual(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.1234560000, "0.123456")]
|
[TestCase(0.1234560000, "0.123456")]
|
||||||
@ -69,22 +67,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void NormalizeTests(decimal input, string expected)
|
public void NormalizeTests(decimal input, string expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.Normalize(input);
|
var result = ExchangeHelpers.Normalize(input);
|
||||||
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
|
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCase("123", "BKR", 32, true, "BKRJK123")]
|
|
||||||
[TestCase("123", "BKR", 32, false, "123")]
|
|
||||||
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
|
|
||||||
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
|
|
||||||
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
|
|
||||||
[TestCase(null, "BKR", 32, true, null)]
|
|
||||||
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
|
|
||||||
{
|
|
||||||
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
|
|
||||||
|
|
||||||
if (expected != null)
|
|
||||||
Assert.That(result, Is.EqualTo(expected));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -36,7 +34,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// act
|
// act
|
||||||
// assert
|
// assert
|
||||||
Assert.Throws(typeof(ArgumentException),
|
Assert.Throws(typeof(ArgumentException),
|
||||||
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -50,121 +48,248 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
|
||||||
Assert.That(options.ApiCredentials.Key == "123");
|
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||||
Assert.That(options.ApiCredentials.Secret == "456");
|
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestApiOptionsAreSet()
|
public void TestApiOptionsAreSet()
|
||||||
{
|
{
|
||||||
// arrange, act
|
// arrange, act
|
||||||
var options = new TestClientOptions();
|
var options = new TestClientOptions
|
||||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
{
|
||||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
BaseAddress = "http://test1.com"
|
||||||
|
},
|
||||||
|
Api2Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("789", "101"),
|
||||||
|
BaseAddress = "http://test2.com"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||||
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||||
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
|
||||||
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||||
|
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||||
|
Assert.AreEqual(options.Api2Options.BaseAddress, "http://test2.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotOverridenApiOptionsAreStillDefault()
|
||||||
|
{
|
||||||
|
// arrange, act
|
||||||
|
var options = new TestClientOptions
|
||||||
|
{
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(options.Api1Options.RateLimitingBehaviour, RateLimitingBehaviour.Wait);
|
||||||
|
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||||
|
Assert.AreEqual(options.Api2Options.BaseAddress, "https://api2.test.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSettingDefaultBaseOptionsAreRespected()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
TestClientOptions.Default = new TestClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
LogLevel = LogLevel.Trace
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var options = new TestClientOptions();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(options.LogLevel, LogLevel.Trace);
|
||||||
|
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||||
|
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSettingDefaultApiOptionsAreRespected()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
TestClientOptions.Default = new TestClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
LogLevel = LogLevel.Trace,
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("456", "789")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var options = new TestClientOptions();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||||
|
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||||
|
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||||
|
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "456");
|
||||||
|
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "789");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSettingDefaultApiOptionsWithSomeOverriddenAreRespected()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
TestClientOptions.Default = new TestClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
LogLevel = LogLevel.Trace,
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("456", "789")
|
||||||
|
},
|
||||||
|
Api2Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("111", "222")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var options = new TestClientOptions
|
||||||
|
{
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("333", "444")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||||
|
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||||
|
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "333");
|
||||||
|
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "444");
|
||||||
|
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "111");
|
||||||
|
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "222");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClientUsesCorrectOptions()
|
public void TestClientUsesCorrectOptions()
|
||||||
{
|
{
|
||||||
var client = new TestRestClient(options => {
|
var client = new TestRestClient(new TestClientOptions()
|
||||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
{
|
||||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("111", "222")
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||||
Assert.That(authProvider1.GetKey() == "111");
|
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||||
Assert.That(authProvider1.GetSecret() == "222");
|
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||||
Assert.That(authProvider2.GetKey() == "333");
|
|
||||||
Assert.That(authProvider2.GetSecret() == "444");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClientUsesCorrectOptionsWithDefault()
|
public void TestClientUsesCorrectOptionsWithDefault()
|
||||||
{
|
{
|
||||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
TestClientOptions.Default = new TestClientOptions()
|
||||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("111", "222")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var client = new TestRestClient();
|
var client = new TestRestClient();
|
||||||
|
|
||||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||||
Assert.That(authProvider1.GetKey() == "111");
|
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||||
Assert.That(authProvider1.GetSecret() == "222");
|
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||||
Assert.That(authProvider2.GetKey() == "123");
|
|
||||||
Assert.That(authProvider2.GetSecret() == "456");
|
|
||||||
|
|
||||||
// Cleanup static values
|
|
||||||
TestClientOptions.Default.ApiCredentials = null;
|
|
||||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||||
{
|
{
|
||||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
TestClientOptions.Default = new TestClientOptions()
|
||||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
|
||||||
|
|
||||||
var client = new TestRestClient(options =>
|
|
||||||
{
|
{
|
||||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("111", "222")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = new TestRestClient(new TestClientOptions
|
||||||
|
{
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new ApiCredentials("333", "444")
|
||||||
|
},
|
||||||
|
Api2Options = new RestApiClientOptions()
|
||||||
|
{
|
||||||
|
BaseAddress = "http://test.com"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
|
||||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
|
||||||
Assert.That(authProvider1.GetKey() == "333");
|
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||||
Assert.That(authProvider1.GetSecret() == "444");
|
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||||
Assert.That(authProvider2.GetKey() == "123");
|
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
|
||||||
Assert.That(authProvider2.GetSecret() == "456");
|
|
||||||
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
|
||||||
|
|
||||||
// Cleanup static values
|
|
||||||
TestClientOptions.Default.ApiCredentials = null;
|
|
||||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
public class TestClientOptions: BaseRestClientOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default options for the futures client
|
/// Default options for the futures client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
public static TestClientOptions Default { get; set; } = new TestClientOptions();
|
||||||
{
|
|
||||||
Environment = new TestEnvironment("test", "https://test.com")
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public TestClientOptions()
|
|
||||||
{
|
|
||||||
Default?.Set(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default receive window for requests
|
/// The default receive window for requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
|
||||||
|
public RestApiClientOptions Api1Options
|
||||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
|
||||||
|
|
||||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
|
||||||
{
|
{
|
||||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
get => _api1Options;
|
||||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
set => _api1Options = new RestApiClientOptions(_api1Options, value);
|
||||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
}
|
||||||
return targetOptions;
|
|
||||||
|
private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/");
|
||||||
|
public RestApiClientOptions Api2Options
|
||||||
|
{
|
||||||
|
get => _api2Options;
|
||||||
|
set => _api2Options = new RestApiClientOptions(_api2Options, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public TestClientOptions(): this(Default)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestClientOptions(TestClientOptions baseOn): base(baseOn)
|
||||||
|
{
|
||||||
|
if (baseOn == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ReceiveWindow = baseOn.ReceiveWindow;
|
||||||
|
|
||||||
|
Api1Options = new RestApiClientOptions(baseOn.Api1Options, null);
|
||||||
|
Api2Options = new RestApiClientOptions(baseOn.Api2Options, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using CryptoExchange.Net.RateLimiting;
|
|
||||||
using System.Net;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Guards;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Filters;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@ -26,14 +25,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var client = new TestRestClient();
|
var client = new TestRestClient();
|
||||||
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||||
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
|
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Api1.Request<TestObject>().Result;
|
var result = client.Request<TestObject>().Result;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result.Success);
|
Assert.IsTrue(result.Success);
|
||||||
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -44,11 +43,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetResponse("{\"property\": 123", out _);
|
client.SetResponse("{\"property\": 123", out _);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Api1.Request<TestObject>().Result;
|
var result = client.Request<TestObject>().Result;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.IsTrue(result.Error != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -59,11 +58,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = await client.Api1.Request<TestObject>();
|
var result = await client.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.IsTrue(result.Error != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -74,12 +73,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = await client.Api1.Request<TestObject>();
|
var result = await client.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.IsTrue(result.Error != null);
|
||||||
Assert.That(result.Error is ServerError);
|
Assert.IsTrue(result.Error is ServerError);
|
||||||
|
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
|
||||||
|
Assert.IsTrue(result.Error.Message.Contains("123"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -90,14 +91,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = await client.Api2.Request<TestObject>();
|
var result = await client.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.IsTrue(result.Error != null);
|
||||||
Assert.That(result.Error is ServerError);
|
Assert.IsTrue(result.Error is ServerError);
|
||||||
Assert.That(result.Error.Code == 123);
|
Assert.IsTrue(result.Error.Code == 123);
|
||||||
Assert.That(result.Error.Message == "Invalid request");
|
Assert.IsTrue(result.Error.Message == "Invalid request");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -105,16 +106,23 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
// act
|
// act
|
||||||
var options = new TestClientOptions();
|
var client = new TestRestClient(new TestClientOptions()
|
||||||
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
|
{
|
||||||
options.Api1Options.OutputOriginalData = true;
|
Api1Options = new RestApiClientOptions
|
||||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
{
|
||||||
var client = new TestBaseClient(options);
|
BaseAddress = "http://test.address.com",
|
||||||
|
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
||||||
|
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||||
|
},
|
||||||
|
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
||||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||||
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||||
|
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||||
@ -128,13 +136,19 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
// act
|
// act
|
||||||
var client = new TestRestClient();
|
var client = new TestRestClient(new TestClientOptions()
|
||||||
|
{
|
||||||
|
Api1Options = new RestApiClientOptions
|
||||||
|
{
|
||||||
|
BaseAddress = "http://test.address.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
||||||
|
|
||||||
client.SetResponse("{}", out var request);
|
client.SetResponse("{}", out var request);
|
||||||
|
|
||||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "TestParam1", "Value1" },
|
{ "TestParam1", "Value1" },
|
||||||
{ "TestParam2", 2 },
|
{ "TestParam2", 2 },
|
||||||
@ -145,13 +159,13 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(request.Method == new HttpMethod(method));
|
Assert.AreEqual(request.Method, new HttpMethod(method));
|
||||||
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
|
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
|
||||||
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
|
||||||
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
|
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
|
||||||
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
|
||||||
Assert.That(request.GetHeaders().First().Key == "TestHeader");
|
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
|
||||||
Assert.That(request.GetHeaders().First().Value.Contains("123"));
|
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -161,22 +175,21 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(1, 2)]
|
[TestCase(1, 2)]
|
||||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
|
|
||||||
var triggered = false;
|
var rateLimiter = new RateLimiter();
|
||||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||||
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
|
||||||
|
|
||||||
for (var i = 0; i < requests + 1; i++)
|
for (var i = 0; i < requests + 1; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(i == requests? triggered : !triggered);
|
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||||
}
|
}
|
||||||
triggered = false;
|
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(!triggered);
|
Assert.IsTrue(result2.Data == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/test1", true)]
|
[TestCase("/sapi/test1", true)]
|
||||||
@ -186,40 +199,35 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/", true)]
|
[TestCase("/sapi/", true)]
|
||||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
|
|
||||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
var rateLimiter = new RateLimiter();
|
||||||
|
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||||
Assert.That(expected);
|
Assert.IsTrue(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/", "/sapi/", true)]
|
[TestCase("/sapi/", "/sapi/", true)]
|
||||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||||
[TestCase("/sapi/test", "/sapi/", false)]
|
[TestCase("/sapi/test", "/sapi/", false)]
|
||||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
|
|
||||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
var rateLimiter = new RateLimiter();
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
|
Assert.IsTrue(result1.Data == 0);
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||||
Assert.That(evnt == null);
|
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
||||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(1, 0.1)]
|
[TestCase(1, 0.1)]
|
||||||
@ -228,22 +236,21 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(1, 2)]
|
[TestCase(1, 2)]
|
||||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
|
|
||||||
bool triggered = false;
|
var rateLimiter = new RateLimiter();
|
||||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||||
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
|
||||||
|
|
||||||
for (var i = 0; i < requests + 1; i++)
|
for (var i = 0; i < requests + 1; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(i == requests ? triggered : !triggered);
|
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||||
}
|
}
|
||||||
triggered = false;
|
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(!triggered);
|
Assert.IsTrue(result2.Data == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/", false)]
|
[TestCase("/", false)]
|
||||||
@ -251,18 +258,17 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/test/123", false)]
|
[TestCase("/sapi/test/123", false)]
|
||||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
|
|
||||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
var rateLimiter = new RateLimiter();
|
||||||
|
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||||
Assert.That(expected);
|
Assert.IsTrue(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,41 +278,53 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/test23", false)]
|
[TestCase("/sapi/test23", false)]
|
||||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
|
||||||
|
var rateLimiter = new RateLimiter();
|
||||||
|
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||||
Assert.That(expected);
|
Assert.IsTrue(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
|
||||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
|
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
|
||||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
|
||||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
|
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
|
||||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||||
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
|
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
[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 RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
log.Level = LogLevel.Trace;
|
||||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
var rateLimiter = new RateLimiter();
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(evnt == null);
|
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
Assert.IsTrue(result1.Data == 0);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||||
@ -314,70 +332,35 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/", "/sapi/test2", true)]
|
[TestCase("/", "/sapi/test2", true)]
|
||||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
var rateLimiter = new RateLimiter();
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(evnt == null);
|
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
Assert.IsTrue(result1.Data == 0);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
|
[TestCase("/sapi/test", true, true, true, false)]
|
||||||
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
|
[TestCase("/sapi/test", false, true, true, false)]
|
||||||
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
|
[TestCase("/sapi/test", false, true, false, true)]
|
||||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
|
[TestCase("/sapi/test", true, true, false, true)]
|
||||||
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
|
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var log = new Log("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
log.Level = LogLevel.Trace;
|
||||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
var rateLimiter = new RateLimiter();
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||||
|
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
Assert.That(evnt == null);
|
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
Assert.IsTrue(result1.Data == 0);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("https://test.com", "https://test.com", true)]
|
|
||||||
[TestCase("https://test2.com", "https://test.com", false)]
|
|
||||||
[TestCase("https://test.com", "https://test2.com", false)]
|
|
||||||
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
|
|
||||||
{
|
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
||||||
Assert.That(evnt == null);
|
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task ConnectionRateLimiterCancel()
|
|
||||||
{
|
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
|
||||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
|
||||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@ -23,15 +17,19 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
//arrange
|
//arrange
|
||||||
//act
|
//act
|
||||||
var client = new TestSocketClient(options =>
|
var client = new TestSocketClient(new TestOptions()
|
||||||
{
|
{
|
||||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
SubOptions = new RestApiClientOptions
|
||||||
options.SubOptions.MaxSocketConnections = 1;
|
{
|
||||||
|
BaseAddress = "http://test.address.com"
|
||||||
|
},
|
||||||
|
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
||||||
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
@ -44,40 +42,37 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
socket.CanConnect = canConnect;
|
socket.CanConnect = canConnect;
|
||||||
|
|
||||||
//act
|
//act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket));
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.That(connectResult.Success == canConnect);
|
Assert.IsTrue(connectResult.Success == canConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options => {
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
|
||||||
});
|
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
|
socket.ShouldReconnect = true;
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
|
var sub = new SocketConnection(client, null, socket);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
Dictionary<string, string> result = null;
|
JToken result = null;
|
||||||
|
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
|
||||||
|
|
||||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
|
||||||
{
|
{
|
||||||
result = messageEvent.Data;
|
result = messageEvent.JsonData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
}));
|
||||||
sub.AddSubscription(subObj);
|
client.ConnectSocketSub(sub);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
socket.InvokeMessage("{\"property\": 123}");
|
||||||
rstEvent.WaitOne(1000);
|
rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result["property"] == "123");
|
Assert.IsTrue((int)result["property"] == 123);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
@ -85,147 +80,111 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options =>
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||||
{
|
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
|
||||||
options.SubOptions.OutputOriginalData = enabled;
|
|
||||||
});
|
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
|
socket.ShouldReconnect = true;
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
|
var sub = new SocketConnection(client, null, socket);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
string original = null;
|
||||||
|
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
|
||||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
|
||||||
{
|
{
|
||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
}));
|
||||||
sub.AddSubscription(subObj);
|
client.ConnectSocketSub(sub);
|
||||||
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage(msgToSend);
|
socket.InvokeMessage("{\"property\": 123}");
|
||||||
rstEvent.WaitOne(1000);
|
rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(original == (enabled ? msgToSend : null));
|
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public void DisconnectedSocket_Should_Reconnect()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
bool reconnected = false;
|
||||||
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
|
var socket = client.CreateSocket();
|
||||||
|
socket.ShouldReconnect = true;
|
||||||
|
socket.CanConnect = true;
|
||||||
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
|
var sub = new SocketConnection(client, null, 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()]
|
[TestCase()]
|
||||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options =>
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
{
|
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
|
||||||
});
|
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(client, null, socket);
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
client.ConnectSocketSub(sub);
|
||||||
|
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
||||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
|
||||||
var ups = new UpdateSubscription(sub, subscription);
|
|
||||||
sub.AddSubscription(subscription);
|
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAsync(ups).Wait();
|
client.UnsubscribeAsync(ups).Wait();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(socket.Connected == false);
|
Assert.IsTrue(socket.Connected == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
var socket1 = client.CreateSocket();
|
var socket1 = client.CreateSocket();
|
||||||
var socket2 = client.CreateSocket();
|
var socket2 = client.CreateSocket();
|
||||||
socket1.CanConnect = true;
|
socket1.CanConnect = true;
|
||||||
socket2.CanConnect = true;
|
socket2.CanConnect = true;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
var sub1 = new SocketConnection(client, null, socket1);
|
||||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
var sub2 = new SocketConnection(client, null, socket2);
|
||||||
client.SubClient.ConnectSocketSub(sub1);
|
client.ConnectSocketSub(sub1);
|
||||||
client.SubClient.ConnectSocketSub(sub2);
|
client.ConnectSocketSub(sub2);
|
||||||
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
|
||||||
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
|
||||||
|
|
||||||
sub1.AddSubscription(subscription1);
|
|
||||||
sub2.AddSubscription(subscription2);
|
|
||||||
var ups1 = new UpdateSubscription(sub1, subscription1);
|
|
||||||
var ups2 = new UpdateSubscription(sub2, subscription2);
|
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAllAsync().Wait();
|
client.UnsubscribeAllAsync().Wait();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(socket1.Connected == false);
|
Assert.IsTrue(socket1.Connected == false);
|
||||||
Assert.That(socket2.Connected == false);
|
Assert.IsTrue(socket2.Connected == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
public void FailingToConnectSocket_Should_ReturnError()
|
public void FailingToConnectSocket_Should_ReturnError()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = false;
|
socket.CanConnect = false;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(client, null, socket);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
var connectResult = client.ConnectSocketSub(sub);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(connectResult.Success);
|
Assert.IsFalse(connectResult.Success);
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
|
||||||
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var channel = "trade_btcusd";
|
|
||||||
var client = new TestSocketClient(opt =>
|
|
||||||
{
|
|
||||||
opt.OutputOriginalData = true;
|
|
||||||
opt.SocketSubscriptionsCombineTarget = 1;
|
|
||||||
});
|
|
||||||
var socket = client.CreateSocket();
|
|
||||||
socket.CanConnect = true;
|
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
|
||||||
|
|
||||||
// act
|
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
|
||||||
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
|
||||||
await sub;
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
|
||||||
public async Task SuccessResponse_Should_ConfirmSubscription()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var channel = "trade_btcusd";
|
|
||||||
var client = new TestSocketClient(opt =>
|
|
||||||
{
|
|
||||||
opt.OutputOriginalData = true;
|
|
||||||
opt.SocketSubscriptionsCombineTarget = 1;
|
|
||||||
});
|
|
||||||
var socket = client.CreateSocket();
|
|
||||||
socket.CanConnect = true;
|
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
|
||||||
|
|
||||||
// act
|
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
|
||||||
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
|
|
||||||
await sub;
|
|
||||||
|
|
||||||
// assert
|
|
||||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,24 +4,21 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.OrderBook;
|
using CryptoExchange.Net.OrderBook;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class SymbolOrderBookTests
|
public class SymbolOrderBookTests
|
||||||
{
|
{
|
||||||
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
|
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
|
||||||
|
|
||||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||||
{
|
{
|
||||||
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
|
public TestableSymbolOrderBook() : base("Test", "BTC/USD", defaultOrderBookOptions)
|
||||||
{
|
{
|
||||||
Initialize(_defaultOrderBookOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -38,12 +35,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
||||||
{
|
{
|
||||||
Status = OrderBookStatus.Synced;
|
Status = OrderBookStatus.Synced;
|
||||||
base._bids.Clear();
|
base.bids.Clear();
|
||||||
foreach (var bid in bids)
|
foreach (var bid in bids)
|
||||||
base._bids.Add(bid.Price, bid);
|
base.bids.Add(bid.Price, bid);
|
||||||
base._asks.Clear();
|
base.asks.Clear();
|
||||||
foreach (var ask in asks)
|
foreach (var ask in asks)
|
||||||
base._asks.Add(ask.Price, ask);
|
base.asks.Add(ask.Price, ask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,31 +54,31 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
||||||
{
|
{
|
||||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
||||||
{
|
{
|
||||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
||||||
{
|
{
|
||||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
|
Assert.IsNotNull(symbolOrderBook.BestOffers);
|
||||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
|
||||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
|
||||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
|
||||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -104,40 +101,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
||||||
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
||||||
|
|
||||||
Assert.That(resultBids.Success);
|
Assert.True(resultBids.Success);
|
||||||
Assert.That(resultAsks.Success);
|
Assert.True(resultAsks.Success);
|
||||||
Assert.That(1.05m == resultBids.Data);
|
Assert.AreEqual(1.05m, resultBids.Data);
|
||||||
Assert.That(1.25m == resultAsks.Data);
|
Assert.AreEqual(1.25m, resultAsks.Data);
|
||||||
Assert.That(1.06666667m == resultBids2.Data);
|
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
||||||
Assert.That(1.23333333m == resultAsks2.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.That(resultBids.Success);
|
|
||||||
Assert.That(resultAsks.Success);
|
|
||||||
Assert.That(1.9m == resultBids.Data);
|
|
||||||
Assert.That(1.61538462m == resultAsks.Data);
|
|
||||||
Assert.That(1.4m == resultBids2.Data);
|
|
||||||
Assert.That(1.23076923m == resultAsks2.Data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,438 +0,0 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using System.Text.Json;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using CryptoExchange.Net.Converters;
|
|
||||||
using CryptoExchange.Net.Testing.Comparers;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
|
||||||
{
|
|
||||||
[TestFixture()]
|
|
||||||
public class SystemTextJsonConverterTests
|
|
||||||
{
|
|
||||||
[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("0.000000", true)]
|
|
||||||
[TestCase("0", true)]
|
|
||||||
[TestCase("", true)]
|
|
||||||
[TestCase(" ", true)]
|
|
||||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
|
|
||||||
Assert.That(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 = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
|
||||||
Assert.That(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 = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
|
||||||
Assert.That(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.That(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.That(output == 1620777600);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000)]
|
|
||||||
[TestCase(1620777600000.000)]
|
|
||||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
|
||||||
Assert.That(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.That(output == 1620777600000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000000)]
|
|
||||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
|
||||||
Assert.That(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.That(output == 1620777600000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000000000)]
|
|
||||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
|
||||||
Assert.That(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.That(output == 1620777600000000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
|
||||||
public void TestDateTimeConverterNull()
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
|
|
||||||
Assert.That(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.That(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.That(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 = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
Assert.That(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 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
|
||||||
Assert.That(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", null)]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
|
||||||
{
|
|
||||||
var result = EnumConverter.ParseString<TestEnum>(value);
|
|
||||||
Assert.That(result == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
|
||||||
[TestCase("true", true)]
|
|
||||||
[TestCase("yes", true)]
|
|
||||||
[TestCase("y", true)]
|
|
||||||
[TestCase("on", true)]
|
|
||||||
[TestCase("-1", false)]
|
|
||||||
[TestCase("0", false)]
|
|
||||||
[TestCase("n", false)]
|
|
||||||
[TestCase("no", false)]
|
|
||||||
[TestCase("false", false)]
|
|
||||||
[TestCase("off", false)]
|
|
||||||
[TestCase("", null)]
|
|
||||||
public void TestBoolConverter(string value, bool? expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
|
||||||
[TestCase("true", true)]
|
|
||||||
[TestCase("yes", true)]
|
|
||||||
[TestCase("y", true)]
|
|
||||||
[TestCase("on", true)]
|
|
||||||
[TestCase("-1", false)]
|
|
||||||
[TestCase("0", false)]
|
|
||||||
[TestCase("n", false)]
|
|
||||||
[TestCase("no", false)]
|
|
||||||
[TestCase("false", false)]
|
|
||||||
[TestCase("off", false)]
|
|
||||||
[TestCase("", false)]
|
|
||||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", 1)]
|
|
||||||
[TestCase("1.1", 1.1)]
|
|
||||||
[TestCase("-1.1", -1.1)]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
[TestCase("", null)]
|
|
||||||
[TestCase("null", null)]
|
|
||||||
[TestCase("1E+2", 100)]
|
|
||||||
[TestCase("1E-2", 0.01)]
|
|
||||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
||||||
public void TestDecimalConverterString(string value, decimal? expected)
|
|
||||||
{
|
|
||||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
|
||||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", 1)]
|
|
||||||
[TestCase("1.1", 1.1)]
|
|
||||||
[TestCase("-1.1", -1.1)]
|
|
||||||
[TestCase("null", null)]
|
|
||||||
[TestCase("1E+2", 100)]
|
|
||||||
[TestCase("1E-2", 0.01)]
|
|
||||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
||||||
public void TestDecimalConverterNumber(string value, decimal? expected)
|
|
||||||
{
|
|
||||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
|
||||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test()]
|
|
||||||
public void TestArrayConverter()
|
|
||||||
{
|
|
||||||
var data = new Test()
|
|
||||||
{
|
|
||||||
Prop1 = 2,
|
|
||||||
Prop2 = null,
|
|
||||||
Prop3 = "123",
|
|
||||||
Prop3Again = "123",
|
|
||||||
Prop4 = null,
|
|
||||||
Prop5 = new Test2
|
|
||||||
{
|
|
||||||
Prop21 = 3,
|
|
||||||
Prop22 = "456"
|
|
||||||
},
|
|
||||||
Prop6 = new Test3
|
|
||||||
{
|
|
||||||
Prop31 = 4,
|
|
||||||
Prop32 = "789"
|
|
||||||
},
|
|
||||||
Prop7 = TestEnum.Two,
|
|
||||||
TestInternal = new Test
|
|
||||||
{
|
|
||||||
Prop1 = 10
|
|
||||||
},
|
|
||||||
Prop8 = new Test3
|
|
||||||
{
|
|
||||||
Prop31 = 5,
|
|
||||||
Prop32 = "101"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
TypeInfoResolver = new SerializationContext()
|
|
||||||
};
|
|
||||||
var serialized = JsonSerializer.Serialize(data);
|
|
||||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
|
||||||
|
|
||||||
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
|
||||||
Assert.That(deserialized.Prop2, Is.Null);
|
|
||||||
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
|
||||||
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
|
||||||
Assert.That(deserialized.Prop4, Is.Null);
|
|
||||||
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
|
||||||
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
|
||||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
|
||||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
|
||||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
|
||||||
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
|
||||||
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
|
|
||||||
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
|
||||||
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
|
||||||
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
|
||||||
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
|
||||||
{
|
|
||||||
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
|
||||||
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
|
||||||
|
|
||||||
var serialized = JsonSerializer.Serialize(symbol);
|
|
||||||
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
|
||||||
|
|
||||||
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
|
|
||||||
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
|
||||||
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
|
||||||
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(0.1, null, null)]
|
|
||||||
[TestCase(0.1, 0.1, null)]
|
|
||||||
[TestCase(0.1, 0.1, 0.1)]
|
|
||||||
[TestCase(null, 0.1, null)]
|
|
||||||
[TestCase(null, 0.1, 0.1)]
|
|
||||||
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
|
||||||
{
|
|
||||||
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
|
||||||
|
|
||||||
var serialized = JsonSerializer.Serialize(symbol);
|
|
||||||
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
|
||||||
|
|
||||||
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
|
||||||
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
|
||||||
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJDecimalObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(DecimalConverter))]
|
|
||||||
[JsonPropertyName("test")]
|
|
||||||
public decimal? Test { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJTimeObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(DateTimeConverter))]
|
|
||||||
[JsonPropertyName("time")]
|
|
||||||
public DateTime? Time { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJEnumObject
|
|
||||||
{
|
|
||||||
public TestEnum? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotNullableSTJEnumObject
|
|
||||||
{
|
|
||||||
public TestEnum Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJBoolObject
|
|
||||||
{
|
|
||||||
public bool? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotNullableSTJBoolObject
|
|
||||||
{
|
|
||||||
public bool Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter<Test>))]
|
|
||||||
record Test
|
|
||||||
{
|
|
||||||
[ArrayProperty(0)]
|
|
||||||
public int Prop1 { get; set; }
|
|
||||||
[ArrayProperty(1)]
|
|
||||||
public int? Prop2 { get; set; }
|
|
||||||
[ArrayProperty(2)]
|
|
||||||
public string Prop3 { get; set; }
|
|
||||||
[ArrayProperty(2)]
|
|
||||||
public string Prop3Again { get; set; }
|
|
||||||
[ArrayProperty(3)]
|
|
||||||
public string Prop4 { get; set; }
|
|
||||||
[ArrayProperty(4)]
|
|
||||||
public Test2 Prop5 { get; set; }
|
|
||||||
[ArrayProperty(5)]
|
|
||||||
public Test3 Prop6 { get; set; }
|
|
||||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
|
||||||
public TestEnum? Prop7 { get; set; }
|
|
||||||
[ArrayProperty(7)]
|
|
||||||
public Test TestInternal { get; set; }
|
|
||||||
[ArrayProperty(8), JsonConversion]
|
|
||||||
public Test3 Prop8 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
|
||||||
record Test2
|
|
||||||
{
|
|
||||||
[ArrayProperty(0)]
|
|
||||||
public int Prop21 { get; set; }
|
|
||||||
[ArrayProperty(1)]
|
|
||||||
public string Prop22 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
record Test3
|
|
||||||
{
|
|
||||||
[JsonPropertyName("prop31")]
|
|
||||||
public int Prop31 { get; set; }
|
|
||||||
[JsonPropertyName("prop32")]
|
|
||||||
public string Prop32 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
|
||||||
public enum TestEnum
|
|
||||||
{
|
|
||||||
[Map("1")]
|
|
||||||
One,
|
|
||||||
[Map("2")]
|
|
||||||
Two,
|
|
||||||
[Map("three", "3")]
|
|
||||||
Three,
|
|
||||||
Four
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonSerializable(typeof(Test))]
|
|
||||||
[JsonSerializable(typeof(Test2))]
|
|
||||||
[JsonSerializable(typeof(Test3))]
|
|
||||||
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
|
|
||||||
[JsonSerializable(typeof(STJBoolObject))]
|
|
||||||
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
|
|
||||||
[JsonSerializable(typeof(STJEnumObject))]
|
|
||||||
[JsonSerializable(typeof(STJDecimalObject))]
|
|
||||||
[JsonSerializable(typeof(STJTimeObject))]
|
|
||||||
internal partial class SerializationContext : JsonSerializerContext
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class SubResponse
|
|
||||||
{
|
|
||||||
|
|
||||||
[JsonPropertyName("action")]
|
|
||||||
public string Action { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("channel")]
|
|
||||||
public string Channel { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public string Status { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class UnsubResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("action")]
|
|
||||||
public string Action { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public string Status { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TestChannelQuery : Query<SubResponse>
|
|
||||||
{
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
|
||||||
{
|
|
||||||
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
|
||||||
{
|
|
||||||
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new CallResult<SubResponse>(new ServerError(message.Data.Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.HandleMessage(connection, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class TestQuery : Query<object>
|
|
||||||
{
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
|
||||||
{
|
|
||||||
ListenerIdentifiers = new HashSet<string> { identifier };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class TestSubscription<T> : Subscription<object, object>
|
|
||||||
{
|
|
||||||
private readonly Action<DataEvent<T>> _handler;
|
|
||||||
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
|
|
||||||
|
|
||||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
|
||||||
{
|
|
||||||
_handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
|
||||||
{
|
|
||||||
var data = (T)message.Data;
|
|
||||||
_handler.Invoke(message.As(data));
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
|
||||||
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class TestSubscriptionWithResponseCheck<T> : Subscription<SubResponse, UnsubResponse>
|
|
||||||
{
|
|
||||||
private readonly Action<DataEvent<T>> _handler;
|
|
||||||
private readonly string _channel;
|
|
||||||
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
|
||||||
{
|
|
||||||
ListenerIdentifiers = new HashSet<string>() { channel };
|
|
||||||
_handler = handler;
|
|
||||||
_channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
|
||||||
{
|
|
||||||
var data = (T)message.Data;
|
|
||||||
_handler.Invoke(message.As(data));
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
|
||||||
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
public class TestBaseClient: BaseClient
|
public class TestBaseClient: BaseClient
|
||||||
{
|
{
|
||||||
public TestSubClient SubClient { get; }
|
public TestBaseClient(): base("Test", new BaseClientOptions())
|
||||||
|
|
||||||
public TestBaseClient(): base(null, "Test")
|
|
||||||
{
|
{
|
||||||
var options = new TestClientOptions();
|
|
||||||
_logger = NullLogger.Instance;
|
|
||||||
Initialize(options);
|
|
||||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||||
{
|
{
|
||||||
_logger = NullLogger.Instance;
|
|
||||||
Initialize(exchangeOptions);
|
|
||||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log(LogLevel verbosity, string data)
|
public void Log(LogLevel verbosity, string data)
|
||||||
{
|
{
|
||||||
_logger.Log(verbosity, data);
|
log.Write(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)
|
public CallResult<T> Deserialize<T>(string data)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
return Deserialize<T>(data, null, null);
|
||||||
var accessor = CreateAccessor();
|
|
||||||
var valid = accessor.Read(stream, true).Result;
|
|
||||||
if (!valid)
|
|
||||||
return new CallResult<T>(new ServerError(data));
|
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
|
||||||
return deserializeResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
public override TimeSpan? GetTimeOffset() => null;
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestAuthProvider : AuthenticationProvider
|
public class TestAuthProvider : AuthenticationProvider
|
||||||
@ -77,11 +34,16 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
public override void 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)
|
||||||
{
|
{
|
||||||
|
bodyParameters = new SortedDictionary<string, object>();
|
||||||
|
uriParameters = new SortedDictionary<string, object>();
|
||||||
|
headers = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKey() => _credentials.Key;
|
public override string Sign(string toSign)
|
||||||
public string GetSecret() => _credentials.Secret;
|
{
|
||||||
|
return toSign;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
using System.Text.Json.Serialization;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
public class TestObject
|
public class TestObject
|
||||||
{
|
{
|
||||||
[JsonPropertyName("other")]
|
[JsonProperty("other")]
|
||||||
public string StringData { get; set; }
|
public string StringData { get; set; }
|
||||||
[JsonPropertyName("intData")]
|
|
||||||
public int IntData { get; set; }
|
public int IntData { get; set; }
|
||||||
[JsonPropertyName("decimalData")]
|
|
||||||
public decimal DecimalData { get; set; }
|
public decimal DecimalData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -11,13 +12,6 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Linq;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@ -26,17 +20,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public TestRestApi1Client Api1 { get; }
|
public TestRestApi1Client Api1 { get; }
|
||||||
public TestRestApi2Client Api2 { get; }
|
public TestRestApi2Client Api2 { get; }
|
||||||
|
|
||||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
public TestRestClient() : this(new TestClientOptions())
|
||||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||||
{
|
{
|
||||||
Initialize(options.Value);
|
Api1 = new TestRestApi1Client(exchangeOptions);
|
||||||
|
Api2 = new TestRestApi2Client(exchangeOptions);
|
||||||
Api1 = new TestRestApi1Client(options.Value);
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
Api2 = new TestRestApi2Client(options.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetResponse(string responseData, out IRequest requestObj)
|
public void SetResponse(string responseData, out IRequest requestObj)
|
||||||
@ -50,24 +42,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, string[]>();
|
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
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.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(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);
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||||
{
|
{
|
||||||
@ -85,15 +68,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
|
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(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>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
@ -109,46 +87,34 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new List<KeyValuePair<string, string[]>>();
|
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
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.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(headers);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
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))
|
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
}
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||||
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))
|
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||||
.Returns(request.Object);
|
}
|
||||||
|
|
||||||
|
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi1Client : RestApiClient
|
public class TestRestApi1Client : RestApiClient
|
||||||
{
|
{
|
||||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options)
|
||||||
{
|
{
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
|
||||||
{
|
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
|
||||||
{
|
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||||
@ -156,7 +122,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
ParameterPositions[method] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
public override TimeSpan GetTimeOffset()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@ -169,7 +135,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
protected override TimeSyncInfo GetTimeSyncInfo()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@ -177,30 +143,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient
|
public class TestRestApi2Client : RestApiClient
|
||||||
{
|
{
|
||||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options)
|
||||||
{
|
{
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
public override TimeSpan GetTimeOffset()
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
|
||||||
{
|
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
|
|
||||||
{
|
|
||||||
var errorData = accessor.Deserialize<TestError>();
|
|
||||||
|
|
||||||
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@ -213,23 +161,34 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
protected override TimeSyncInfo GetTimeSyncInfo()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestError
|
public class TestAuthProvider : AuthenticationProvider
|
||||||
{
|
{
|
||||||
[JsonPropertyName("errorCode")]
|
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||||
public int ErrorCode { get; set; }
|
{
|
||||||
[JsonPropertyName("errorMessage")]
|
}
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
|
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||||
|
bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||||
|
headers = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ParseErrorTestRestClient: TestRestClient
|
public class ParseErrorTestRestClient: TestRestClient
|
||||||
{
|
{
|
||||||
public ParseErrorTestRestClient() { }
|
public ParseErrorTestRestClient() { }
|
||||||
|
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||||
|
|
||||||
|
protected override Error ParseErrorResponse(JToken error)
|
||||||
|
{
|
||||||
|
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,132 +1,123 @@
|
|||||||
//using System;
|
using System;
|
||||||
//using System.IO;
|
using System.Security.Authentication;
|
||||||
//using System.Net.WebSockets;
|
using System.Text;
|
||||||
//using System.Security.Authentication;
|
using System.Threading.Tasks;
|
||||||
//using System.Text;
|
using CryptoExchange.Net.Interfaces;
|
||||||
//using System.Threading.Tasks;
|
using CryptoExchange.Net.Objects;
|
||||||
//using CryptoExchange.Net.Interfaces;
|
|
||||||
//using CryptoExchange.Net.Objects;
|
|
||||||
|
|
||||||
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
//{
|
{
|
||||||
// public class TestSocket: IWebsocket
|
public class TestSocket: IWebsocket
|
||||||
// {
|
{
|
||||||
// public bool CanConnect { get; set; }
|
public bool CanConnect { get; set; }
|
||||||
// public bool Connected { get; set; }
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
// public event Func<Task> OnClose;
|
public event Action OnClose;
|
||||||
//#pragma warning disable 0067
|
public event Action<string> OnMessage;
|
||||||
// public event Func<Task> OnReconnected;
|
public event Action<Exception> OnError;
|
||||||
// public event Func<Task> OnReconnecting;
|
public event Action OnOpen;
|
||||||
// public event Func<int, Task> OnRequestRateLimited;
|
|
||||||
//#pragma warning restore 0067
|
|
||||||
// public event Func<int, Task> OnRequestSent;
|
|
||||||
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
|
||||||
// public event Func<Exception, Task> OnError;
|
|
||||||
// public event Func<Task> OnOpen;
|
|
||||||
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
|
||||||
|
|
||||||
// public int Id { get; }
|
public int Id { get; }
|
||||||
// public bool ShouldReconnect { get; set; }
|
public bool ShouldReconnect { get; set; }
|
||||||
// public TimeSpan Timeout { get; set; }
|
public TimeSpan Timeout { get; set; }
|
||||||
// public Func<string, string> DataInterpreterString { get; set; }
|
public Func<string, string> DataInterpreterString { get; set; }
|
||||||
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||||
// public DateTime? DisconnectTime { get; set; }
|
public DateTime? DisconnectTime { get; set; }
|
||||||
// public string Url { get; }
|
public string Url { get; }
|
||||||
// public bool IsClosed => !Connected;
|
public bool IsClosed => !Connected;
|
||||||
// public bool IsOpen => Connected;
|
public bool IsOpen => Connected;
|
||||||
// public bool PingConnection { get; set; }
|
public bool PingConnection { get; set; }
|
||||||
// public TimeSpan PingInterval { get; set; }
|
public TimeSpan PingInterval { get; set; }
|
||||||
// public SslProtocols SSLProtocols { get; set; }
|
public SslProtocols SSLProtocols { get; set; }
|
||||||
// public Encoding Encoding { get; set; }
|
public Encoding Encoding { get; set; }
|
||||||
|
|
||||||
// public int ConnectCalls { get; private set; }
|
public int ConnectCalls { get; private set; }
|
||||||
// public bool Reconnecting { get; set; }
|
public bool Reconnecting { get; set; }
|
||||||
// public string Origin { get; set; }
|
public string Origin { get; set; }
|
||||||
// public int? RatelimitPerSecond { get; set; }
|
public int? RatelimitPerSecond { get; set; }
|
||||||
|
|
||||||
// public double IncomingKbps => throw new NotImplementedException();
|
public double IncomingKbps => throw new NotImplementedException();
|
||||||
|
|
||||||
// public Uri Uri => new Uri("");
|
public Uri Uri => new Uri("");
|
||||||
|
|
||||||
// public TimeSpan KeepAliveInterval { get; set; }
|
public static int lastId = 0;
|
||||||
|
public static object lastIdLock = new object();
|
||||||
|
|
||||||
// public static int lastId = 0;
|
public TestSocket()
|
||||||
// public static object lastIdLock = new object();
|
{
|
||||||
|
lock (lastIdLock)
|
||||||
|
{
|
||||||
|
Id = lastId + 1;
|
||||||
|
lastId++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// public TestSocket()
|
public Task<bool> ConnectAsync()
|
||||||
// {
|
{
|
||||||
// lock (lastIdLock)
|
Connected = CanConnect;
|
||||||
// {
|
ConnectCalls++;
|
||||||
// Id = lastId + 1;
|
if (CanConnect)
|
||||||
// lastId++;
|
InvokeOpen();
|
||||||
// }
|
return Task.FromResult(CanConnect);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public Task<CallResult> ConnectAsync()
|
public void Send(string data)
|
||||||
// {
|
{
|
||||||
// Connected = CanConnect;
|
if(!Connected)
|
||||||
// ConnectCalls++;
|
throw new Exception("Socket not connected");
|
||||||
// if (CanConnect)
|
}
|
||||||
// InvokeOpen();
|
|
||||||
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public bool Send(int requestId, string data, int weight)
|
public void Reset()
|
||||||
// {
|
{
|
||||||
// if(!Connected)
|
}
|
||||||
// throw new Exception("Socket not connected");
|
|
||||||
// OnRequestSent?.Invoke(requestId);
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void Reset()
|
public Task CloseAsync()
|
||||||
// {
|
{
|
||||||
// }
|
Connected = false;
|
||||||
|
DisconnectTime = DateTime.UtcNow;
|
||||||
|
OnClose?.Invoke();
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
// public Task CloseAsync()
|
public void SetProxy(string host, int port)
|
||||||
// {
|
{
|
||||||
// Connected = false;
|
throw new NotImplementedException();
|
||||||
// DisconnectTime = DateTime.UtcNow;
|
}
|
||||||
// OnClose?.Invoke();
|
public void Dispose()
|
||||||
// return Task.FromResult(0);
|
{
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public void SetProxy(string host, int port)
|
public void InvokeClose()
|
||||||
// {
|
{
|
||||||
// throw new NotImplementedException();
|
Connected = false;
|
||||||
// }
|
DisconnectTime = DateTime.UtcNow;
|
||||||
// public void Dispose()
|
OnClose?.Invoke();
|
||||||
// {
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// public void InvokeClose()
|
public void InvokeOpen()
|
||||||
// {
|
{
|
||||||
// Connected = false;
|
OnOpen?.Invoke();
|
||||||
// DisconnectTime = DateTime.UtcNow;
|
}
|
||||||
// Reconnecting = true;
|
|
||||||
// OnClose?.Invoke();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void InvokeOpen()
|
public void InvokeMessage(string data)
|
||||||
// {
|
{
|
||||||
// OnOpen?.Invoke();
|
OnMessage?.Invoke(data);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public void InvokeMessage(string data)
|
public void SetProxy(ApiProxy proxy)
|
||||||
// {
|
{
|
||||||
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
throw new NotImplementedException();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public void SetProxy(ApiProxy proxy)
|
public void InvokeError(Exception error)
|
||||||
// {
|
{
|
||||||
// throw new NotImplementedException();
|
OnError?.Invoke(error);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public void InvokeError(Exception error)
|
public async Task ProcessAsync()
|
||||||
// {
|
{
|
||||||
// OnError?.Invoke(error);
|
while (Connected)
|
||||||
// }
|
await Task.Delay(50);
|
||||||
// public Task ReconnectAsync() => Task.CompletedTask;
|
}
|
||||||
// }
|
}
|
||||||
//}
|
}
|
||||||
|
@ -1,139 +1,87 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using CryptoExchange.Net.Testing.Implementations;
|
using Newtonsoft.Json.Linq;
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
internal class TestSocketClient: BaseSocketClient
|
public class TestSocketClient: BaseSocketClient
|
||||||
{
|
{
|
||||||
public TestSubSocketClient SubClient { get; }
|
public TestSubSocketClient SubClient { get; }
|
||||||
|
|
||||||
/// <summary>
|
public TestSocketClient() : this(new TestOptions())
|
||||||
/// Create a new instance of KucoinSocketClient
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
|
||||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
|
||||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
||||||
{
|
{
|
||||||
Initialize(options.Value);
|
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
||||||
|
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
|
||||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
public TestSocket CreateSocket()
|
||||||
{
|
{
|
||||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
return (TestSocket)CreateSocket("123");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||||
|
{
|
||||||
|
return ConnectSocketAsync(sub).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestEnvironment : TradeEnvironment
|
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||||
{
|
{
|
||||||
public string TestAddress { get; }
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public TestEnvironment(string name, string url) : base(name)
|
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
|
||||||
|
out CallResult<object> callResult)
|
||||||
{
|
{
|
||||||
TestAddress = url;
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
public class TestOptions: BaseSocketClientOptions
|
||||||
{
|
{
|
||||||
public static TestSocketOptions Default = new TestSocketOptions
|
public ApiClientOptions SubOptions { get; set; } = new ApiClientOptions();
|
||||||
{
|
|
||||||
Environment = new TestEnvironment("Live", "https://test.test")
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public TestSocketOptions()
|
|
||||||
{
|
|
||||||
Default?.Set(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
|
||||||
|
|
||||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
|
||||||
{
|
|
||||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
|
||||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
|
||||||
return targetOptions;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestSubSocketClient : SocketApiClient
|
public class TestSubSocketClient : SocketApiClient
|
||||||
{
|
{
|
||||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
|
||||||
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
|
||||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
|
||||||
|
|
||||||
public Subscription TestSubscription { get; private set; } = null;
|
public TestSubSocketClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||||
|
|
||||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected internal override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
internal IWebsocket CreateSocketInternal(string address)
|
|
||||||
{
|
|
||||||
return CreateSocket(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
|
||||||
{
|
|
||||||
return ConnectSocketAsync(sub, default).Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
|
||||||
{
|
|
||||||
if (!message.IsJson)
|
|
||||||
{
|
|
||||||
return "topic";
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = message.GetValue<string>(_channelPath);
|
|
||||||
id ??= message.GetValue<string>(_topicPath);
|
|
||||||
|
|
||||||
return message.GetValue<string>(_actionPath) + "-" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
|
||||||
{
|
|
||||||
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
|
|
||||||
return SubscribeAsync(TestSubscription, ct);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
|
||||||
{
|
|
||||||
[JsonSerializable(typeof(string))]
|
|
||||||
[JsonSerializable(typeof(int))]
|
|
||||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
|
||||||
[JsonSerializable(typeof(IDictionary<string, string>))]
|
|
||||||
[JsonSerializable(typeof(Dictionary<string, object>))]
|
|
||||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
|
||||||
[JsonSerializable(typeof(TestObject))]
|
|
||||||
internal partial class TestSerializerContext : JsonSerializerContext
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,9 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -37,10 +35,6 @@ Global
|
|||||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -48,7 +42,6 @@ Global
|
|||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||||
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||||
|
@ -1,6 +1 @@
|
|||||||
[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,7 +5,6 @@ namespace CryptoExchange.Net.Attributes
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Map a enum entry to string values
|
/// Map a enum entry to string values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
|
||||||
public class MapAttribute : Attribute
|
public class MapAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#if NETSTANDARD2_0
|
#if !NETSTANDARD2_1
|
||||||
namespace System.Diagnostics.CodeAnalysis
|
namespace System.Diagnostics.CodeAnalysis
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,51 +1,66 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using System.Security;
|
||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
using System.Text;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api credentials, used to sign requests accessing private endpoints
|
/// Api credentials, used to sign requests accessing private endpoints
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiCredentials
|
public class ApiCredentials: IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api key / label to authenticate requests
|
/// The api key to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; set; }
|
public SecureString? Key { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api secret or private key to authenticate requests
|
/// The api secret to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Secret { get; set; }
|
public SecureString? Secret { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api passphrase. Not needed on all exchanges
|
/// The private key to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Pass { get; set; }
|
public PrivateKey? PrivateKey { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the credentials
|
/// Create Api credentials providing a private key for authentication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiCredentialsType CredentialType { get; set; }
|
/// <param name="privateKey">The private key used for signing</param>
|
||||||
|
public ApiCredentials(PrivateKey privateKey)
|
||||||
|
{
|
||||||
|
PrivateKey = privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
/// Create Api credentials providing an api key and secret for authentication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The api key / label used for identification</param>
|
/// <param name="key">The api key used for identification</param>
|
||||||
/// <param name="secret">The api secret or private key used for signing</param>
|
/// <param name="secret">The api secret used for signing</param>
|
||||||
/// <param name="pass">The api pass for the key. Not always needed</param>
|
public ApiCredentials(SecureString key, SecureString secret)
|
||||||
/// <param name="credentialType">The type of credentials</param>
|
{
|
||||||
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
if (key == null || secret == null)
|
||||||
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
|
|
||||||
|
Key = key;
|
||||||
|
Secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
public ApiCredentials(string key, string secret)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
|
|
||||||
CredentialType = credentialType;
|
Key = key.ToSecureString();
|
||||||
Key = key;
|
Secret = secret.ToSecureString();
|
||||||
Secret = secret;
|
|
||||||
Pass = pass;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -54,7 +69,61 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ApiCredentials Copy()
|
public virtual ApiCredentials Copy()
|
||||||
{
|
{
|
||||||
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
if (PrivateKey == null)
|
||||||
|
// Use .GetString() to create a copy of the SecureString
|
||||||
|
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||||
|
else
|
||||||
|
return new ApiCredentials(PrivateKey!.Copy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputStream">The stream containing the json data</param>
|
||||||
|
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
||||||
|
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
||||||
|
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true);
|
||||||
|
|
||||||
|
var stringData = reader.ReadToEnd();
|
||||||
|
var jsonData = stringData.ToJToken();
|
||||||
|
if(jsonData == null)
|
||||||
|
throw new ArgumentException("Input stream not valid json data");
|
||||||
|
|
||||||
|
var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
|
||||||
|
var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
|
||||||
|
|
||||||
|
if (key == null || secret == null)
|
||||||
|
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||||
|
|
||||||
|
Key = key.ToSecureString();
|
||||||
|
Secret = secret.ToSecureString();
|
||||||
|
|
||||||
|
inputStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try get the value of a key from a JToken
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string? TryGetValue(JToken data, string key)
|
||||||
|
{
|
||||||
|
if (data[key] == null)
|
||||||
|
return null;
|
||||||
|
return (string) data[key]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Key?.Dispose();
|
||||||
|
Secret?.Dispose();
|
||||||
|
PrivateKey?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
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,6 +1,4 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Converters;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -16,38 +14,26 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AuthenticationProvider
|
public abstract class AuthenticationProvider
|
||||||
{
|
{
|
||||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided credentials
|
/// The provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal readonly ApiCredentials _credentials;
|
public ApiCredentials Credentials { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Byte representation of the secret
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected byte[] _sBytes;
|
protected byte[] _sBytes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the API key of the current credentials
|
|
||||||
/// </summary>
|
|
||||||
public string ApiKey => _credentials.Key!;
|
|
||||||
/// <summary>
|
|
||||||
/// Get the Passphrase of the current credentials
|
|
||||||
/// </summary>
|
|
||||||
public string? Pass => _credentials.Pass;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="credentials"></param>
|
/// <param name="credentials"></param>
|
||||||
protected AuthenticationProvider(ApiCredentials credentials)
|
protected AuthenticationProvider(ApiCredentials credentials)
|
||||||
{
|
{
|
||||||
if (credentials.Key == null || credentials.Secret == null)
|
if (credentials.Secret == null)
|
||||||
throw new ArgumentException("ApiKey/Secret needed");
|
throw new ArgumentException("ApiKey/Secret needed");
|
||||||
|
|
||||||
_credentials = credentials;
|
Credentials = credentials;
|
||||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -56,24 +42,24 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="apiClient">The Api client sending the request</param>
|
/// <param name="apiClient">The Api client sending the request</param>
|
||||||
/// <param name="uri">The uri for the request</param>
|
/// <param name="uri">The uri for the request</param>
|
||||||
/// <param name="method">The method of 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="auth">If the requests should be authenticated</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="requestBodyFormat">The formatting of the request body</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="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="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||||
/// <param name="headers">The headers that should be send with the request</param>
|
/// <param name="headers">The headers that should be send with the request</param>
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
|
||||||
public abstract void AuthenticateRequest(
|
public abstract void AuthenticateRequest(
|
||||||
RestApiClient apiClient,
|
RestApiClient apiClient,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
ref IDictionary<string, object>? uriParameters,
|
Dictionary<string, object> providedParameters,
|
||||||
ref IDictionary<string, object>? bodyParameters,
|
|
||||||
ref Dictionary<string, string>? headers,
|
|
||||||
bool auth,
|
bool auth,
|
||||||
ArrayParametersSerialization arraySerialization,
|
ArrayParametersSerialization arraySerialization,
|
||||||
HttpMethodParameterPosition parameterPosition,
|
HttpMethodParameterPosition parameterPosition,
|
||||||
RequestBodyFormat requestBodyFormat
|
out SortedDictionary<string, object> uriParameters,
|
||||||
|
out SortedDictionary<string, object> bodyParameters,
|
||||||
|
out Dictionary<string, string> headers
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -87,17 +73,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 sign the data and return the bytes
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA256Bytes(byte[] data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA256.Create();
|
|
||||||
return encryptor.ComputeHash(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA256 sign the data and return the hash
|
/// SHA256 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -111,19 +86,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes): BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes): BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 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 SignSHA256(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA256.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA384 sign the data and return the hash
|
/// SHA384 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -137,41 +99,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
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(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(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>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA384Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA384Bytes(byte[] data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
return encryptor.ComputeHash(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA512 sign the data and return the hash
|
/// SHA512 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -185,41 +112,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
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(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(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>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA512Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA512Bytes(byte[] data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
return encryptor.ComputeHash(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MD5 sign the data and return the hash
|
/// MD5 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -233,30 +125,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
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(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = MD5.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(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>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignMD5Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = MD5.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA256 sign the data and return the hash
|
/// HMACSHA256 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -264,18 +132,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="outputType">String type</param>
|
/// <param name="outputType">String type</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||||
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <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(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
{
|
||||||
using var encryptor = new HMACSHA256(_sBytes);
|
using var encryptor = new HMACSHA256(_sBytes);
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,18 +145,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="outputType">String type</param>
|
/// <param name="outputType">String type</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||||
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <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(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
{
|
||||||
using var encryptor = new HMACSHA384(_sBytes);
|
using var encryptor = new HMACSHA384(_sBytes);
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,80 +174,23 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA256 sign the data
|
/// Sign a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="toSign"></param>
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
public virtual string Sign(string toSign)
|
||||||
{
|
{
|
||||||
using var rsa = CreateRSA();
|
return toSign;
|
||||||
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>
|
/// <summary>
|
||||||
/// SHA384 sign the data
|
/// Sign a byte array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="toSign"></param>
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
public virtual byte[] Sign(byte[] toSign)
|
||||||
{
|
{
|
||||||
using var rsa = CreateRSA();
|
return toSign;
|
||||||
using var sha384 = SHA384.Create();
|
|
||||||
var hash = sha384.ComputeHash(data);
|
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var rsa = CreateRSA();
|
|
||||||
using var sha512 = SHA512.Create();
|
|
||||||
var hash = sha512.ComputeHash(data);
|
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RSA CreateRSA()
|
|
||||||
{
|
|
||||||
var rsa = RSA.Create();
|
|
||||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
|
||||||
{
|
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
|
||||||
// Read from pem private key
|
|
||||||
var key = _credentials.Secret!
|
|
||||||
.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!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception("Invalid credentials type");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rsa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -407,14 +200,10 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string BytesToHexString(byte[] buff)
|
protected static string BytesToHexString(byte[] buff)
|
||||||
{
|
{
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
return Convert.ToHexString(buff);
|
|
||||||
#else
|
|
||||||
var result = string.Empty;
|
var result = string.Empty;
|
||||||
foreach (var t in buff)
|
foreach (var t in buff)
|
||||||
result += t.ToString("X2");
|
result += t.ToString("X2");
|
||||||
return result;
|
return result;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -432,9 +221,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected DateTime GetTimestamp(RestApiClient apiClient)
|
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -442,48 +231,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
|
||||||
{
|
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return the serialized request body
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serializer"></param>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
|
||||||
{
|
|
||||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
|
||||||
return serializer.Serialize(value);
|
|
||||||
else
|
|
||||||
return serializer.Serialize(parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
110
CryptoExchange.Net/Authentication/PrivateKey.cs
Normal file
110
CryptoExchange.Net/Authentication/PrivateKey.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,53 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Caching
|
|
||||||
{
|
|
||||||
internal class MemoryCache
|
|
||||||
{
|
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The key identifier</param>
|
|
||||||
/// <param name="value">Cache value</param>
|
|
||||||
public void Add(string key, object value)
|
|
||||||
{
|
|
||||||
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
|
||||||
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a cached value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The key identifier</param>
|
|
||||||
/// <param name="maxAge">The max age of the cached entry</param>
|
|
||||||
/// <returns>Cached value if it was in cache</returns>
|
|
||||||
public object? Get(string key, TimeSpan maxAge)
|
|
||||||
{
|
|
||||||
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
|
|
||||||
_cache.TryRemove(item.Key, out _);
|
|
||||||
|
|
||||||
_cache.TryGetValue(key, out CacheItem? value);
|
|
||||||
if (value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CacheItem
|
|
||||||
{
|
|
||||||
public DateTime CacheTime { get; }
|
|
||||||
public object Value { get; }
|
|
||||||
|
|
||||||
public CacheItem(DateTime cacheTime, object value)
|
|
||||||
{
|
|
||||||
CacheTime = cacheTime;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +1,89 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base API for all API clients
|
/// Base API for all API clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
public abstract class BaseApiClient: IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
private ApiCredentials? _apiCredentials;
|
||||||
/// Logger
|
private AuthenticationProvider? _authenticationProvider;
|
||||||
/// </summary>
|
private bool _created;
|
||||||
protected ILogger _logger;
|
private bool _disposing;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If we are disposing
|
|
||||||
/// </summary>
|
|
||||||
protected bool _disposing;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
public AuthenticationProvider? AuthenticationProvider
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_created && !_disposing && _apiCredentials != null)
|
||||||
|
{
|
||||||
|
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
|
||||||
|
_created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _authenticationProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The environment this client communicates to
|
/// Where to put the parameters for requests with different Http methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BaseAddress { get; }
|
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>
|
/// <summary>
|
||||||
/// Output the original string data along with the deserialized object
|
/// Request body content type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OutputOriginalData { get; }
|
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Authenticated => ApiCredentials != null;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public ApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Whether or not we need to manually parse an error instead of relying on the http status code
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiOptions ApiOptions { get; }
|
public bool manualParseError = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client Options
|
/// How to serialize array parameters when making requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ExchangeOptions ClientOptions { get; }
|
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 base address for this API client
|
||||||
|
/// </summary>
|
||||||
|
internal protected string BaseAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Api client options
|
||||||
|
/// </summary>
|
||||||
|
internal ApiClientOptions Options { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="options">Client options</param>
|
||||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
/// <param name="apiOptions">Api client options</param>
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions)
|
||||||
/// <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;
|
Options = apiOptions;
|
||||||
|
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
|
||||||
ClientOptions = clientOptions;
|
BaseAddress = apiOptions.BaseAddress;
|
||||||
ApiOptions = apiOptions;
|
|
||||||
OutputOriginalData = outputOriginalData;
|
|
||||||
BaseAddress = baseAddress;
|
|
||||||
ApiCredentials = apiCredentials?.Copy();
|
|
||||||
|
|
||||||
if (ApiCredentials != null)
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -85,33 +94,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
public void SetApiCredentials(ApiCredentials credentials)
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
|
||||||
{
|
{
|
||||||
ApiCredentials = credentials?.Copy();
|
_apiCredentials = credentials?.Copy();
|
||||||
if (ApiCredentials != null)
|
_created = false;
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
_authenticationProvider = null;
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
|
||||||
{
|
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
|
||||||
if (ApiCredentials != null)
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
|
_apiCredentials?.Dispose();
|
||||||
|
AuthenticationProvider?.Credentials?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,96 +1,74 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Logging;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base for all clients, websocket client and rest client
|
/// The base for all clients, websocket client and rest client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseClient : IDisposable
|
public abstract class BaseClient : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Version of the CryptoExchange.Net base library
|
|
||||||
/// </summary>
|
|
||||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Version of the client implementation
|
|
||||||
/// </summary>
|
|
||||||
public Version ExchangeLibVersion
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock(_versionLock)
|
|
||||||
{
|
|
||||||
if (_exchangeVersion == null)
|
|
||||||
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
|
||||||
|
|
||||||
return _exchangeVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the API the client is for
|
/// The name of the API the client is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
internal string Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api clients in this client
|
/// Api clients in this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The log object
|
/// The log object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ILogger _logger;
|
protected internal Log log;
|
||||||
|
/// <summary>
|
||||||
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
|
/// </summary>
|
||||||
|
protected static int lastId;
|
||||||
|
/// <summary>
|
||||||
|
/// Lock for id generating
|
||||||
|
/// </summary>
|
||||||
|
protected static object idLock = new object();
|
||||||
|
|
||||||
private readonly object _versionLock = new object();
|
/// <summary>
|
||||||
private Version _exchangeVersion;
|
/// A default serializer
|
||||||
|
/// </summary>
|
||||||
|
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||||
|
Culture = CultureInfo.InvariantCulture
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided client options
|
/// Provided client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ExchangeOptions ClientOptions { get; private set; }
|
public BaseClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
/// <param name="options">The options for this client</param>
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
protected BaseClient(string name, BaseClientOptions options)
|
||||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
{
|
{
|
||||||
Exchange = exchange;
|
log = new Log(name);
|
||||||
}
|
log.UpdateWriters(options.LogWriters);
|
||||||
|
log.Level = options.LogLevel;
|
||||||
/// <summary>
|
|
||||||
/// Initialize the client with the specified options
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
/// <exception cref="ArgumentNullException"></exception>
|
|
||||||
protected virtual void Initialize(ExchangeOptions options)
|
|
||||||
{
|
|
||||||
if (options == null)
|
|
||||||
throw new ArgumentNullException(nameof(options));
|
|
||||||
|
|
||||||
ClientOptions = options;
|
ClientOptions = options;
|
||||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
Name = name;
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||||
/// <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>
|
/// <summary>
|
||||||
@ -99,22 +77,209 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="apiClient">The client</param>
|
/// <param name="apiClient">The client</param>
|
||||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||||
{
|
{
|
||||||
if (ClientOptions == null)
|
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
|
||||||
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);
|
ApiClients.Add(apiClient);
|
||||||
return apiClient;
|
return apiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply the options delegate to a new options instance
|
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
/// <param name="data">The data to parse</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<JToken> ValidateJson(string data)
|
||||||
{
|
{
|
||||||
var opts = new T();
|
if (string.IsNullOrEmpty(data))
|
||||||
del?.Invoke(opts);
|
{
|
||||||
return opts;
|
var info = "Empty data object received";
|
||||||
|
log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new CallResult<JToken>(JToken.Parse(data));
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a string into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="data">The data to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
|
{
|
||||||
|
var tokenResult = ValidateJson(data);
|
||||||
|
if (!tokenResult)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||||
|
return new CallResult<T>( tokenResult.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a JToken into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="obj">The data to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
|
{
|
||||||
|
serializer ??= defaultSerializer;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||||
|
log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||||
|
log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||||
|
log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a stream into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="stream">The stream to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||||
|
{
|
||||||
|
serializer ??= defaultSerializer;
|
||||||
|
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 (ClientOptions.OutputOriginalData || log.Level == LogLevel.Trace)
|
||||||
|
{
|
||||||
|
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||||
|
var result = Deserialize<T>(data, serializer, requestId);
|
||||||
|
if(ClientOptions.OutputOriginalData)
|
||||||
|
result.OriginalData = data;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||||
|
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||||
|
using var jsonReader = new JsonTextReader(reader);
|
||||||
|
log.Write(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]";
|
||||||
|
}
|
||||||
|
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
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]";
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||||
|
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static int NextId()
|
||||||
|
{
|
||||||
|
lock (idLock)
|
||||||
|
{
|
||||||
|
lastId += 1;
|
||||||
|
return lastId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -122,7 +287,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
log.Write(LogLevel.Debug, "Disposing client");
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,490 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base rest client
|
/// Base rest client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating requests. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request headers to be sent with each request
|
||||||
|
/// </summary>
|
||||||
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client options
|
||||||
|
/// </summary>
|
||||||
|
public new BaseRestClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
/// <param name="options">The options for this client</param>
|
||||||
|
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
ClientOptions = options;
|
||||||
|
RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetApiCredentials(ApiCredentials credentials)
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a request to the uri and returns if it was successful
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The API client the request is for</param>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="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(RestApiClient apiClient,
|
||||||
|
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 request = await PrepareRequestAsync(apiClient, 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>(apiClient, request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||||
|
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="apiClient">The API client the request is for</param>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="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>(
|
||||||
|
RestApiClient apiClient,
|
||||||
|
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
|
||||||
|
{
|
||||||
|
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult<T>(request.Error!);
|
||||||
|
|
||||||
|
return await GetResponseAsync<T>(apiClient, request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares a request to be sent to the server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The API client the request is for</param>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="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(RestApiClient apiClient,
|
||||||
|
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 syncTimeResult = await apiClient.SyncTimeAsync().ConfigureAwait(false);
|
||||||
|
if (!syncTimeResult)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||||
|
return syncTimeResult.As<IRequest>(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreRatelimit)
|
||||||
|
{
|
||||||
|
foreach (var limiter in apiClient.RateLimiters)
|
||||||
|
{
|
||||||
|
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!limitResult.Success)
|
||||||
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signed && apiClient.AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||||
|
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||||
|
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
||||||
|
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.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)}]"));
|
||||||
|
|
||||||
|
apiClient.TotalRequestsMade++;
|
||||||
|
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||||
|
return new CallResult<IRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The client making the request</param>
|
||||||
|
/// <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>(
|
||||||
|
BaseApiClient apiClient,
|
||||||
|
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 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 (apiClient.manualParseError)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(responseStream);
|
||||||
|
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+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, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
|
// Let the library implementation see if it is an error response, and if so parse the error
|
||||||
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
|
if (error != null)
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
|
||||||
|
// Not an error, so continue deserializing
|
||||||
|
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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, ClientOptions.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, ClientOptions.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, ClientOptions.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, 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, ClientOptions.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);
|
||||||
|
log.Write(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) : parseResult.Error!;
|
||||||
|
if(error.Code == null || error.Code == 0)
|
||||||
|
error.Code = (int)response.StatusCode;
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 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();
|
||||||
|
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException canceledException)
|
||||||
|
{
|
||||||
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
|
{
|
||||||
|
// Cancellation token canceled by caller
|
||||||
|
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Request timed out
|
||||||
|
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// Creates a request object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The API client the request is for</param>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <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(
|
||||||
|
RestApiClient apiClient,
|
||||||
|
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 (apiClient.AuthenticationProvider != null)
|
||||||
|
apiClient.AuthenticationProvider.AuthenticateRequest(
|
||||||
|
apiClient,
|
||||||
|
uri,
|
||||||
|
method,
|
||||||
|
parameters,
|
||||||
|
signed,
|
||||||
|
arraySerialization,
|
||||||
|
parameterPosition,
|
||||||
|
out uriParameters,
|
||||||
|
out bodyParameters,
|
||||||
|
out headers);
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
foreach(var param in parameters)
|
||||||
|
{
|
||||||
|
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||||
|
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||||
|
$"should return provided parameters in either the uri or body parameters output");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
|
uri = uri.SetParameters(uriParameters, 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 = apiClient.requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
|
if (bodyParameters.Any())
|
||||||
|
WriteParamBody(apiClient, request, bodyParameters, contentType);
|
||||||
|
else
|
||||||
|
request.SetContent(apiClient.requestBodyEmptyContent, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the parameters of the request to the request object body
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The client making the request</param>
|
||||||
|
/// <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(BaseApiClient apiClient, IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||||
|
{
|
||||||
|
if (apiClient.requestBodyFormat == RequestBodyFormat.Json)
|
||||||
|
{
|
||||||
|
// Write the parameters as json in the body
|
||||||
|
var stringData = JsonConvert.SerializeObject(parameters);
|
||||||
|
request.SetContent(stringData, contentType);
|
||||||
|
}
|
||||||
|
else if (apiClient.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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for socket client implementations
|
/// Base for socket client implementations
|
||||||
@ -18,28 +22,622 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||||
{
|
{
|
||||||
#region fields
|
#region fields
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating sockets. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of socket connections currently connecting/connected
|
||||||
|
/// </summary>
|
||||||
|
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Semaphore used while creating sockets
|
||||||
|
/// </summary>
|
||||||
|
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of concurrent socket connections
|
||||||
|
/// </summary>
|
||||||
|
protected int MaxSocketConnections { get; set; } = 9999;
|
||||||
|
/// <summary>
|
||||||
|
/// Keep alive interval for websocket connection
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
protected bool disposing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true; data which is a response to a query will also be distributed to subscriptions
|
||||||
|
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool ContinueOnQueryResponse { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of outgoing messages per socket per second
|
||||||
|
/// </summary>
|
||||||
|
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
public double IncomingKbps
|
||||||
/// <inheritdoc />
|
{
|
||||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
get
|
||||||
/// <inheritdoc />
|
{
|
||||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
if (!socketConnections.Any())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client options
|
||||||
|
/// </summary>
|
||||||
|
public new BaseSocketClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
/// <param name="name">The name of the exchange this client is for</param>
|
/// <param name="options">The options for this client</param>
|
||||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
ClientOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetApiCredentials(ApiCredentials credentials)
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a delegate to be used for processing data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="byteHandler">Handler for byte data</param>
|
||||||
|
/// <param name="stringHandler">Handler for string data</param>
|
||||||
|
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
|
||||||
|
{
|
||||||
|
dataInterpreterBytes = byteHandler;
|
||||||
|
dataInterpreterString = stringHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect to an url and listen for data on the BaseAddress
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||||
|
/// <param name="apiClient">The API client the subscription is for</param>
|
||||||
|
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||||
|
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||||
|
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||||
|
/// <param name="dataHandler">The handler of update data</param>
|
||||||
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return SubscribeAsync(apiClient, apiClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect to an url and listen for data
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||||
|
/// <param name="apiClient">The API client the subscription is for</param>
|
||||||
|
/// <param name="url">The URL to connect to</param>
|
||||||
|
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||||
|
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||||
|
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||||
|
/// <param name="dataHandler">The handler of update data</param>
|
||||||
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, 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;
|
||||||
|
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
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get a new or existing socket connection
|
||||||
|
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
||||||
|
|
||||||
|
// Add a subscription on the socket connection
|
||||||
|
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
|
||||||
|
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!);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if(!released)
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketConnection.PausedActivity)
|
||||||
|
{
|
||||||
|
log.Write(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)
|
||||||
|
{
|
||||||
|
// Send the request and wait for answer
|
||||||
|
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
||||||
|
if (!subResult)
|
||||||
|
{
|
||||||
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No request to be sent, so just mark the subscription as comfirmed
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketConnection.ShouldReconnect = true;
|
||||||
|
if (ct != default)
|
||||||
|
{
|
||||||
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||||
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription completed");
|
||||||
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the subscribe request and waits for a response to that request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The connection to send the request on</param>
|
||||||
|
/// <param name="request">The request to send, will be serialized to json</param>
|
||||||
|
/// <param name="subscription">The subscription the request is for</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||||
|
{
|
||||||
|
CallResult<object>? callResult = null;
|
||||||
|
await socketConnection.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (callResult?.Success == true)
|
||||||
|
{
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(callResult== null)
|
||||||
|
return new CallResult<bool>(new ServerError("No response on subscription request received"));
|
||||||
|
|
||||||
|
return new CallResult<bool>(callResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Expected result type</typeparam>
|
||||||
|
/// <param name="apiClient">The API client the query is for</param>
|
||||||
|
/// <param name="request">The request to send, will be serialized to json</param>
|
||||||
|
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, object request, bool authenticated)
|
||||||
|
{
|
||||||
|
return QueryAsync<T>(apiClient, apiClient.Options.BaseAddress, request, authenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a query on a socket connection and wait for the response
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected result type</typeparam>
|
||||||
|
/// <param name="apiClient">The API client the query is for</param>
|
||||||
|
/// <param name="url">The url for the request</param>
|
||||||
|
/// <param name="request">The request to send</param>
|
||||||
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, 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(apiClient, url, authenticated);
|
||||||
|
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||||
|
{
|
||||||
|
// Can release early when only a single sub per connection
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
released = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the query request and waits for the result
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected result type</typeparam>
|
||||||
|
/// <param name="socket">The connection to send and wait on</param>
|
||||||
|
/// <param name="request">The request to send</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
||||||
|
{
|
||||||
|
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||||
|
await socket.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data =>
|
||||||
|
{
|
||||||
|
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
dataResult = callResult;
|
||||||
|
return true;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return dataResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socket">The connection to check</param>
|
||||||
|
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||||
|
{
|
||||||
|
if (socket.Connected)
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
|
||||||
|
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult<bool>(connectResult.Error!);
|
||||||
|
|
||||||
|
if (!authenticated || socket.Authenticated)
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
|
||||||
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||||
|
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||||
|
return new CallResult<bool>(result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Authenticated = true;
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 query that was send (the request parameter).
|
||||||
|
/// For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
|
||||||
|
/// anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
|
||||||
|
/// if not some other method has be implemented to match the messages).
|
||||||
|
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of response that is expected on the query</typeparam>
|
||||||
|
/// <param name="socketConnection">The socket connection</param>
|
||||||
|
/// <param name="request">The request that a response is awaited for</param>
|
||||||
|
/// <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);
|
||||||
|
/// <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
|
||||||
|
/// anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
|
||||||
|
/// if not some other method has be implemented to match the messages).
|
||||||
|
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The socket connection</param>
|
||||||
|
/// <param name="subscription">A subscription that waiting for a subscription response</param>
|
||||||
|
/// <param name="request">The request that the subscription sent</param>
|
||||||
|
/// <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 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(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(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>
|
||||||
|
/// <param name="connection">The connection on which to unsubscribe</param>
|
||||||
|
/// <param name="subscriptionToUnsub">The subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional handler to interpolate data before sending it to the handlers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal virtual JToken ProcessTokenData(JToken message)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a subscription to a connection
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of data the subscription expects</typeparam>
|
||||||
|
/// <param name="request">The request of the subscription</param>
|
||||||
|
/// <param name="identifier">The identifier of the subscription (can be null if request param is used)</param>
|
||||||
|
/// <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>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual SocketSubscription AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler)
|
||||||
|
{
|
||||||
|
void InternalHandler(MessageEvent messageEvent)
|
||||||
|
{
|
||||||
|
if (typeof(T) == typeof(string))
|
||||||
|
{
|
||||||
|
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
||||||
|
dataHandler(new DataEvent<T>(stringData, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||||
|
if (!desResult)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataHandler(new DataEvent<T>(desResult.Data, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = request == null
|
||||||
|
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, InternalHandler)
|
||||||
|
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, InternalHandler);
|
||||||
|
connection.AddSubscription(subscription);
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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(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 socketConnections.Values)
|
||||||
|
connection.AddSubscription(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The API client the connection is for</param>
|
||||||
|
/// <param name="address">The address the socket is for</param>
|
||||||
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual SocketConnection GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
||||||
|
{
|
||||||
|
var socketResult = socketConnections.Where(s => s.Value.Uri.ToString().TrimEnd('/') == address.TrimEnd('/')
|
||||||
|
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
||||||
|
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
||||||
|
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new socket
|
||||||
|
var socket = CreateSocket(address);
|
||||||
|
var socketConnection = new SocketConnection(this, apiClient, socket);
|
||||||
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
foreach (var kvp in genericHandlers)
|
||||||
|
{
|
||||||
|
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, kvp.Value);
|
||||||
|
socketConnection.AddSubscription(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return socketConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process an unhandled message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The token that wasn't processed</param>
|
||||||
|
protected virtual void HandleUnhandledMessage(JToken token)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect a socket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The socket to connect</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
||||||
|
{
|
||||||
|
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
socketConnection.Dispose();
|
||||||
|
return new CallResult<bool>(new CantConnectError());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a socket for an address
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address the socket should connect to</param>
|
||||||
|
/// <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 (ClientOptions.Proxy != null)
|
||||||
|
socket.SetProxy(ClientOptions.Proxy);
|
||||||
|
|
||||||
|
socket.KeepAliveInterval = KeepAliveInterval;
|
||||||
|
socket.Timeout = ClientOptions.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());
|
||||||
|
};
|
||||||
|
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(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||||
|
{
|
||||||
|
if (objGetter == null)
|
||||||
|
throw new ArgumentNullException(nameof(objGetter));
|
||||||
|
|
||||||
|
periodicEvent = new AsyncResetEvent();
|
||||||
|
periodicTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!disposing)
|
||||||
|
{
|
||||||
|
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
|
||||||
|
if (disposing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (var socketConnection in socketConnections.Values)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!socketConnection.Connected)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var obj = objGetter(socketConnection);
|
||||||
|
if (obj == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socketConnection.Send(obj);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -49,14 +647,26 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||||
{
|
{
|
||||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
|
||||||
|
SocketSubscription? subscription = null;
|
||||||
|
SocketConnection? connection = null;
|
||||||
|
foreach(var socket in socketConnections.Values.ToList())
|
||||||
{
|
{
|
||||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
subscription = socket.GetSubscription(subscriptionId);
|
||||||
if (result)
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
connection = socket;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subscription == null || connection == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
log.Write(LogLevel.Information, "Closing subscription " + subscriptionId);
|
||||||
|
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe an update subscription
|
/// Unsubscribe an update subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -67,7 +677,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (subscription == null)
|
if (subscription == null)
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
log.Write(LogLevel.Information, "Closing subscription " + subscription.Id);
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,56 +687,29 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
|
log.Write(LogLevel.Information, $"Closing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||||
var tasks = new List<Task>();
|
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.ReconnectingAllConnections(CurrentConnections);
|
var socketList = socketConnections.Values;
|
||||||
var tasks = new List<Task>();
|
foreach (var sub in socketList)
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
tasks.Add(sub.CloseAsync());
|
||||||
{
|
|
||||||
tasks.Add(client.ReconnectAsync());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Dispose the client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetSubscriptionsState()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
var result = new StringBuilder();
|
disposing = true;
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
periodicEvent?.Set();
|
||||||
{
|
periodicEvent?.Dispose();
|
||||||
result.AppendLine(client.GetSubscriptionsState());
|
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||||
}
|
_ = UnsubscribeAllAsync();
|
||||||
|
semaphoreSlim?.Dispose();
|
||||||
return result.ToString();
|
base.Dispose();
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the state of all socket api clients
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
|
||||||
{
|
|
||||||
var result = new List<SocketApiClient.SocketApiClientState>();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
|
||||||
{
|
|
||||||
result.Add(client.GetState());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base crypto client
|
|
||||||
/// </summary>
|
|
||||||
public class CryptoBaseClient : IDisposable
|
|
||||||
{
|
|
||||||
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Service provider
|
|
||||||
/// </summary>
|
|
||||||
protected readonly IServiceProvider? _serviceProvider;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoBaseClient() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider"></param>
|
|
||||||
public CryptoBaseClient(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_serviceCache = new Dictionary<Type, object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try get a client by type for the service collection
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <returns></returns>
|
|
||||||
public T TryGet<T>(Func<T> createFunc)
|
|
||||||
{
|
|
||||||
var type = typeof(T);
|
|
||||||
if (_serviceCache.TryGetValue(type, out var value))
|
|
||||||
return (T)value;
|
|
||||||
|
|
||||||
if (_serviceProvider == null)
|
|
||||||
{
|
|
||||||
// Create with default options
|
|
||||||
var createResult = createFunc();
|
|
||||||
_serviceCache.Add(typeof(T), createResult!);
|
|
||||||
return createResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = _serviceProvider.GetService<T>()
|
|
||||||
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
|
|
||||||
_serviceCache.Add(type, result!);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_serviceCache.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoRestClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider"></param>
|
|
||||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoSocketClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider"></param>
|
|
||||||
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,700 +1,74 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Caching;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.RateLimiting;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
|
||||||
using CryptoExchange.Net.Requests;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base rest API client for interacting with a REST API
|
/// Base rest API client for interacting with a REST API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class RestApiClient : BaseApiClient, IRestApiClient
|
public abstract class RestApiClient: BaseApiClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
/// Get time sync info for an API client
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract TimeSyncInfo GetTimeSyncInfo();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
/// Get time offset for an API client
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract TimeSpan GetTimeOffset();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public abstract TimeSpan? GetTimeOffset();
|
/// Total amount of requests made with this API client
|
||||||
|
/// </summary>
|
||||||
/// <inheritdoc />
|
|
||||||
public int TotalRequestsMade { get; set; }
|
public int TotalRequestsMade { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request body content type
|
/// Options for this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How to serialize array parameters when making requests
|
/// List of rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
|
||||||
/// </summary>
|
|
||||||
protected internal string RequestBodyEmptyContent = "{}";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request headers to be sent with each request
|
|
||||||
/// </summary>
|
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether parameters need to be ordered
|
|
||||||
/// </summary>
|
|
||||||
protected internal bool OrderParameters { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parameter order comparer
|
|
||||||
/// </summary>
|
|
||||||
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
|
|
||||||
|
|
||||||
/// <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 },
|
|
||||||
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Memory cache
|
|
||||||
/// </summary>
|
|
||||||
private readonly static MemoryCache _cache = new MemoryCache();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </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="options">The base client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
|
||||||
: base(logger,
|
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
|
||||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
{
|
||||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
var rateLimiters = new List<IRateLimiter>();
|
||||||
}
|
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||||
|
rateLimiters.Add(rateLimiter);
|
||||||
/// <summary>
|
RateLimiters = rateLimiters;
|
||||||
/// Create a message accessor instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract IStreamMessageAccessor CreateAccessor();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a serializer instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract IMessageSerializer CreateSerializer();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a request to the base address based on the request definition
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
|
||||||
/// <param name="parameters">Request parameters</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
|
||||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<WebCallResult> SendAsync(
|
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
|
||||||
ParameterCollection? parameters,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
int? weight = null)
|
|
||||||
{
|
|
||||||
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
|
||||||
return result.AsDataless();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a request to the base address based on the request definition
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Response type</typeparam>
|
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
|
||||||
/// <param name="parameters">Request parameters</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
|
||||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
|
||||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
|
||||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
|
||||||
ParameterCollection? parameters,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
int? weight = null,
|
|
||||||
int? weightSingleLimiter = null,
|
|
||||||
string? rateLimitKeySuffix = null)
|
|
||||||
{
|
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
|
||||||
return SendAsync<T>(
|
|
||||||
baseAddress,
|
|
||||||
definition,
|
|
||||||
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
|
||||||
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
|
||||||
cancellationToken,
|
|
||||||
additionalHeaders,
|
|
||||||
weight,
|
|
||||||
weightSingleLimiter,
|
|
||||||
rateLimitKeySuffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a request to the base address based on the request definition
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Response type</typeparam>
|
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
|
||||||
/// <param name="uriParameters">Request query parameters</param>
|
|
||||||
/// <param name="bodyParameters">Request body parameters</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
|
||||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
|
||||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
|
||||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
|
||||||
ParameterCollection? uriParameters,
|
|
||||||
ParameterCollection? bodyParameters,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
int? weight = null,
|
|
||||||
int? weightSingleLimiter = null,
|
|
||||||
string? rateLimitKeySuffix = null)
|
|
||||||
{
|
|
||||||
string? cacheKey = null;
|
|
||||||
if (ShouldCache(definition))
|
|
||||||
{
|
|
||||||
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
|
|
||||||
_logger.CheckingCache(cacheKey);
|
|
||||||
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
|
||||||
if (cachedValue != null)
|
|
||||||
{
|
|
||||||
_logger.CacheHit(cacheKey);
|
|
||||||
var original = (WebCallResult<T>)cachedValue;
|
|
||||||
return original.Cached();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.CacheNotHit(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentTry = 0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
currentTry++;
|
|
||||||
var requestId = ExchangeHelpers.NextId();
|
|
||||||
|
|
||||||
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false);
|
|
||||||
if (!prepareResult)
|
|
||||||
return new WebCallResult<T>(prepareResult.Error!);
|
|
||||||
|
|
||||||
var request = CreateRequest(
|
|
||||||
requestId,
|
|
||||||
baseAddress,
|
|
||||||
definition,
|
|
||||||
uriParameters,
|
|
||||||
bodyParameters,
|
|
||||||
additionalHeaders);
|
|
||||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
|
||||||
TotalRequestsMade++;
|
|
||||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (result.Error is not CancellationRequestedError)
|
|
||||||
{
|
|
||||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
|
||||||
if (!result)
|
|
||||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
|
|
||||||
else
|
|
||||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.RestApiCancellationRequested(result.RequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (result.Success &&
|
|
||||||
ShouldCache(definition))
|
|
||||||
{
|
|
||||||
_cache.Add(cacheKey!, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="requestId">Request id</param>
|
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
|
||||||
/// <param name="weight">Override the request weight for this request</param>
|
|
||||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
|
||||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="Exception"></exception>
|
|
||||||
protected virtual async Task<CallResult> PrepareAsync(
|
|
||||||
int requestId,
|
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
int? weight = null,
|
|
||||||
int? weightSingleLimiter = null,
|
|
||||||
string? rateLimitKeySuffix = null)
|
|
||||||
{
|
|
||||||
// Time sync
|
|
||||||
if (definition.Authenticated)
|
|
||||||
{
|
|
||||||
if (AuthenticationProvider == null)
|
|
||||||
{
|
|
||||||
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
|
||||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
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.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
|
|
||||||
return syncTimeResult.AsDataless();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting
|
|
||||||
var requestWeight = weight ?? definition.Weight;
|
|
||||||
if (requestWeight != 0)
|
|
||||||
{
|
|
||||||
if (definition.RateLimitGate == null)
|
|
||||||
throw new Exception("Ratelimit gate not set when request weight is not 0");
|
|
||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
|
||||||
{
|
|
||||||
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult)
|
|
||||||
return new CallResult(limitResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Endpoint specific rate limiting
|
|
||||||
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
|
|
||||||
{
|
|
||||||
if (definition.RateLimitGate == null)
|
|
||||||
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
|
||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
|
||||||
{
|
|
||||||
var singleRequestWeight = weightSingleLimiter ?? 1;
|
|
||||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult)
|
|
||||||
return new CallResult(limitResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a request object
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="requestId">Id of the request</param>
|
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
|
||||||
/// <param name="uriParameters">The query parameters of the request</param>
|
|
||||||
/// <param name="bodyParameters">The body parameters of the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IRequest CreateRequest(
|
|
||||||
int requestId,
|
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
|
||||||
ParameterCollection? uriParameters,
|
|
||||||
ParameterCollection? bodyParameters,
|
|
||||||
Dictionary<string, string>? additionalHeaders)
|
|
||||||
{
|
|
||||||
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
|
|
||||||
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
|
|
||||||
|
|
||||||
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
|
||||||
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
|
||||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
|
||||||
|
|
||||||
Dictionary<string, string>? headers = null;
|
|
||||||
if (AuthenticationProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
AuthenticationProvider.AuthenticateRequest(
|
|
||||||
this,
|
|
||||||
uri,
|
|
||||||
definition.Method,
|
|
||||||
ref uriParams,
|
|
||||||
ref bodyParams,
|
|
||||||
ref headers,
|
|
||||||
definition.Authenticated,
|
|
||||||
arraySerialization,
|
|
||||||
parameterPosition,
|
|
||||||
bodyFormat
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
|
||||||
if (uriParams != null)
|
|
||||||
uri = uri.SetParameters(uriParams, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
|
||||||
request.Accept = Constants.JsonContentHeader;
|
|
||||||
|
|
||||||
if (headers != null)
|
|
||||||
{
|
|
||||||
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 = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
|
||||||
if (bodyParams != null && bodyParams.Count != 0)
|
|
||||||
WriteParamBody(request, bodyParams, contentType);
|
|
||||||
else
|
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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="gate">The ratelimit gate used</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
|
||||||
IRequest request,
|
|
||||||
IRateLimitGate? gate,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
Stream? responseStream = null;
|
|
||||||
IResponse? response = null;
|
|
||||||
IStreamMessageAccessor? accessor = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
sw.Stop();
|
|
||||||
var statusCode = response.StatusCode;
|
|
||||||
var headers = response.ResponseHeaders;
|
|
||||||
var responseLength = response.ContentLength;
|
|
||||||
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
|
||||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
|
||||||
|
|
||||||
accessor = CreateAccessor();
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
// Error response
|
|
||||||
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Error error;
|
|
||||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
|
||||||
{
|
|
||||||
var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
|
|
||||||
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
|
|
||||||
{
|
|
||||||
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
|
|
||||||
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
error = rateError;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.Code == null || error.Code == 0)
|
|
||||||
error.Code = (int)response.StatusCode;
|
|
||||||
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
|
||||||
}
|
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
|
||||||
if (typeof(T) == typeof(object))
|
|
||||||
// Success status code and expected empty response, assume it's correct
|
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
|
||||||
|
|
||||||
if (!valid)
|
|
||||||
{
|
|
||||||
// Invalid json
|
|
||||||
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception);
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Json response received
|
|
||||||
var parsedError = TryParseError(response.ResponseHeaders, accessor);
|
|
||||||
if (parsedError != null)
|
|
||||||
{
|
|
||||||
if (parsedError is ServerRateLimitError rateError)
|
|
||||||
{
|
|
||||||
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
|
|
||||||
{
|
|
||||||
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
|
|
||||||
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success status code, but TryParseError determined it was an error response
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
|
||||||
}
|
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
|
||||||
}
|
|
||||||
catch (HttpRequestException requestException)
|
|
||||||
{
|
|
||||||
// Request exception, can't reach server for instance
|
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException));
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException canceledException)
|
|
||||||
{
|
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
|
||||||
{
|
|
||||||
// Cancellation token canceled by caller
|
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Request timed out
|
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
accessor?.Clear();
|
|
||||||
responseStream?.Close();
|
|
||||||
response?.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.
|
|
||||||
/// 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="accessor">Data accessor</param>
|
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
|
||||||
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
|
||||||
|
|
||||||
/// <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="gate">The rate limit gate the call used</param>
|
|
||||||
/// <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 async Task<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
|
|
||||||
{
|
|
||||||
if (tries >= 2)
|
|
||||||
// Only retry once
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (callResult.Error is ServerRateLimitError
|
|
||||||
&& ClientOptions.RateLimiterEnabled
|
|
||||||
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
|
|
||||||
&& gate != null)
|
|
||||||
{
|
|
||||||
var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false);
|
|
||||||
if (retryTime == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60))
|
|
||||||
{
|
|
||||||
_logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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, IDictionary<string, object> parameters, string contentType)
|
|
||||||
{
|
|
||||||
if (contentType == Constants.JsonContentHeader)
|
|
||||||
{
|
|
||||||
// Write the parameters as json in the body
|
|
||||||
string stringData;
|
|
||||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
|
||||||
stringData = CreateSerializer().Serialize(value);
|
|
||||||
else
|
|
||||||
stringData = CreateSerializer().Serialize(parameters);
|
|
||||||
request.SetContent(stringData, contentType);
|
|
||||||
}
|
|
||||||
else if (contentType == Constants.FormContentHeader)
|
|
||||||
{
|
|
||||||
// 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) or ratelimit error (429 or 418)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpStatusCode">The response status code</param>
|
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
|
||||||
/// <param name="accessor">Data accessor</param>
|
|
||||||
/// <param name="exception">Exception</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
|
|
||||||
{
|
|
||||||
return new ServerError(null, "Unknown request error", exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpStatusCode">The response status code</param>
|
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
|
||||||
/// <param name="accessor">Data accessor</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
|
||||||
{
|
|
||||||
// Handle retry after header
|
|
||||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
if (retryAfterHeader.Value?.Any() != true)
|
|
||||||
return new ServerRateLimitError();
|
|
||||||
|
|
||||||
var value = retryAfterHeader.Value.First();
|
|
||||||
if (int.TryParse(value, out var seconds))
|
|
||||||
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
|
||||||
|
|
||||||
if (DateTime.TryParse(value, out var datetime))
|
|
||||||
return new ServerRateLimitError() { RetryAfter = datetime };
|
|
||||||
|
|
||||||
return new ServerRateLimitError();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create the parameter IDictionary
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
|
|
||||||
{
|
|
||||||
if (!OrderParameters)
|
|
||||||
return parameters;
|
|
||||||
|
|
||||||
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Server time</returns>
|
/// <returns>Server time</returns>
|
||||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void SetOptions<T>(UpdateOptions<T> options)
|
|
||||||
{
|
|
||||||
base.SetOptions(options);
|
|
||||||
|
|
||||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@ -718,17 +92,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time offset between local and server
|
// Calculate time offset between local and server
|
||||||
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
|
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
|
||||||
timeSyncParams.UpdateTimeOffset(offset);
|
timeSyncParams.UpdateTimeOffset(offset);
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldCache(RequestDefinition definition)
|
|
||||||
=> ClientOptions.CachingEnabled
|
|
||||||
&& definition.Method == HttpMethod.Get
|
|
||||||
&& !definition.PreventCaching;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,861 +1,19 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.RateLimiting;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base socket API client for interaction with a websocket API
|
/// Base socket API client for interaction with a websocket API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
public abstract class SocketApiClient : BaseApiClient
|
||||||
{
|
{
|
||||||
#region Fields
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of socket connections currently connecting/connected
|
|
||||||
/// </summary>
|
|
||||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Semaphore used while creating sockets
|
|
||||||
/// </summary>
|
|
||||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Keep alive interval for websocket connection
|
|
||||||
/// </summary>
|
|
||||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Keep alive timeout for websocket connection
|
|
||||||
/// </summary>
|
|
||||||
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
/// <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 List<SystemSubscription> systemSubscriptions = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
|
||||||
/// </summary>
|
|
||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The rate limiters
|
|
||||||
/// </summary>
|
|
||||||
protected internal IRateLimitGate? RateLimiter { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The max size a websocket message size can be
|
|
||||||
/// </summary>
|
|
||||||
protected internal int? MessageSendSizeLimit { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Periodic task registrations
|
|
||||||
/// </summary>
|
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of address to keep an alive connection to
|
|
||||||
/// </summary>
|
|
||||||
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to allow multiple subscriptions with the same topic on the same connection
|
|
||||||
/// </summary>
|
|
||||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public double IncomingKbps
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (socketConnections.IsEmpty)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int CurrentConnections => socketConnections.Count;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int CurrentSubscriptions
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (socketConnections.IsEmpty)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">log</param>
|
/// <param name="options">The base client options</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>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||||
: base(logger,
|
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
|
||||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a message accessor instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal abstract IByteMessageAccessor CreateAccessor();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a serializer instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal abstract IMessageSerializer CreateSerializer();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Keep an open connection to this url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="auth"></param>
|
|
||||||
protected virtual void SetDedicatedConnection(string url, bool auth)
|
|
||||||
{
|
|
||||||
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a query to periodically send on each connection
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="identifier"></param>
|
|
||||||
/// <param name="interval"></param>
|
|
||||||
/// <param name="queryDelegate"></param>
|
|
||||||
/// <param name="callback"></param>
|
|
||||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
|
||||||
{
|
|
||||||
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
|
||||||
{
|
|
||||||
Identifier = identifier,
|
|
||||||
Callback = callback,
|
|
||||||
Interval = interval,
|
|
||||||
QueryDelegate = queryDelegate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect to an url and listen for data on the BaseAddress
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscription">The subscription</param>
|
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
|
|
||||||
{
|
|
||||||
return SubscribeAsync(BaseAddress, subscription, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect to an url and listen for data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url">The URL to connect to</param>
|
|
||||||
/// <param name="subscription">The subscription</param>
|
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (_disposing)
|
|
||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
|
||||||
|
|
||||||
if (subscription.Authenticated && AuthenticationProvider == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
|
||||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
|
||||||
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
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException tce)
|
|
||||||
{
|
|
||||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// Get a new or existing socket connection
|
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
|
||||||
if (!socketResult)
|
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
|
||||||
var success = socketConnection.AddSubscription(subscription);
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
|
|
||||||
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, subscription.Authenticated, ct).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (!released)
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
|
||||||
{
|
|
||||||
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
|
||||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var waitEvent = new AsyncResetEvent(false);
|
|
||||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
|
||||||
if (subQuery != null)
|
|
||||||
{
|
|
||||||
// Send the request and wait for answer
|
|
||||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
|
|
||||||
if (!subResult)
|
|
||||||
{
|
|
||||||
waitEvent?.Set();
|
|
||||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
|
||||||
if (isTimeout && subscription.Confirmed)
|
|
||||||
{
|
|
||||||
// No response received, but the subscription did receive updates. We'll assume success
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
if (ct != default)
|
|
||||||
{
|
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
|
||||||
{
|
|
||||||
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
|
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
waitEvent?.Set();
|
|
||||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
|
||||||
/// <param name="query">The query</param>
|
|
||||||
/// <param name="ct">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
return QueryAsync(BaseAddress, query, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a query on a socket connection and wait for the response
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
|
||||||
/// <param name="url">The url for the request</param>
|
|
||||||
/// <param name="query">The query</param>
|
|
||||||
/// <param name="ct">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (_disposing)
|
|
||||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested)
|
|
||||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
|
||||||
var released = false;
|
|
||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
|
||||||
if (!socketResult)
|
|
||||||
return socketResult.As<THandlerResponse>(default);
|
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
|
||||||
|
|
||||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
|
||||||
{
|
|
||||||
// Can release early when only a single sub per connection
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
released = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult<THandlerResponse>(connectResult.Error!);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (!released)
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
|
||||||
{
|
|
||||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
|
||||||
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested)
|
|
||||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
|
||||||
|
|
||||||
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socket">The connection to check</param>
|
|
||||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
|
||||||
/// <param name="ct">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (socket.Connected)
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return connectResult;
|
|
||||||
|
|
||||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
|
||||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
|
|
||||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Authenticate a socket connection
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socket">Socket to authenticate</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
|
||||||
{
|
|
||||||
if (AuthenticationProvider == null)
|
|
||||||
return new CallResult(new NoApiCredentialsError());
|
|
||||||
|
|
||||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
|
||||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
|
||||||
if (authRequest != null)
|
|
||||||
{
|
|
||||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
_logger.AuthenticationFailed(socket.SocketId);
|
|
||||||
if (socket.Connected)
|
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
|
||||||
return new CallResult(result.Error)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Authenticated(socket.SocketId);
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.Authenticated = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Should return the request which can be used to authenticate a socket connection
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="systemSubscription">The subscription</param>
|
|
||||||
protected void AddSystemSubscription(SystemSubscription systemSubscription)
|
|
||||||
{
|
|
||||||
systemSubscriptions.Add(systemSubscription);
|
|
||||||
foreach (var connection in socketConnections.Values)
|
|
||||||
connection.AddSubscription(systemSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
|
||||||
{
|
|
||||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscription">The subscription</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
|
||||||
{
|
|
||||||
return Task.FromResult(CallResult.SuccessResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
|
||||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
|
|
||||||
{
|
|
||||||
var socketQuery = 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)
|
|
||||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
|
||||||
&& s.Value.Connected);
|
|
||||||
|
|
||||||
SocketConnection connection;
|
|
||||||
if (!dedicatedRequestConnection)
|
|
||||||
{
|
|
||||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
|
||||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
|
||||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
|
||||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connection != null)
|
|
||||||
{
|
|
||||||
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
|
||||||
return new CallResult<SocketConnection>(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
|
||||||
if (!connectionAddress)
|
|
||||||
{
|
|
||||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
|
||||||
return connectionAddress.As<SocketConnection>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionAddress.Data != address)
|
|
||||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
|
||||||
|
|
||||||
// Create new socket
|
|
||||||
var socket = CreateSocket(connectionAddress.Data!);
|
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
|
||||||
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
|
||||||
if (dedicatedRequestConnection)
|
|
||||||
{
|
|
||||||
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
|
|
||||||
{
|
|
||||||
IsDedicatedRequestConnection = dedicatedRequestConnection,
|
|
||||||
Authenticated = authenticated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var ptg in PeriodicTaskRegistrations)
|
|
||||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
|
||||||
|
|
||||||
foreach (var systemSubscription in systemSubscriptions)
|
|
||||||
socketConnection.AddSubscription(systemSubscription);
|
|
||||||
|
|
||||||
return new CallResult<SocketConnection>(socketConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process an unhandled message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message that wasn't processed</param>
|
|
||||||
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process connect rate limited
|
|
||||||
/// </summary>
|
|
||||||
protected async virtual Task HandleConnectRateLimitedAsync()
|
|
||||||
{
|
|
||||||
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
|
|
||||||
{
|
|
||||||
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
|
||||||
_logger.AddingRetryAfterGuard(retryAfter);
|
|
||||||
RateLimiter ??= new RateLimitGate("Connection");
|
|
||||||
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect a socket
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socketConnection">The socket to connect</param>
|
|
||||||
/// <param name="ct">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
|
||||||
if (connectResult)
|
|
||||||
{
|
|
||||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
|
||||||
return connectResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
socketConnection.Dispose();
|
|
||||||
return connectResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.ReconnectPolicy)
|
|
||||||
{
|
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
|
||||||
KeepAliveTimeout = KeepAliveTimeout,
|
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
|
||||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
|
||||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
|
||||||
Proxy = ClientOptions.Proxy,
|
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
|
||||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a socket for an address
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">The address the socket should connect to</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IWebsocket CreateSocket(string address)
|
|
||||||
{
|
|
||||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
|
||||||
_logger.SocketCreatedForAddress(socket.Id, address);
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribe an update subscription
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task<bool> UnsubscribeAsync(int subscriptionId)
|
|
||||||
{
|
|
||||||
Subscription? subscription = null;
|
|
||||||
SocketConnection? connection = null;
|
|
||||||
foreach (var socket in socketConnections.Values.ToList())
|
|
||||||
{
|
|
||||||
subscription = socket.GetSubscription(subscriptionId);
|
|
||||||
if (subscription != null)
|
|
||||||
{
|
|
||||||
connection = socket;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscription == null || connection == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_logger.UnsubscribingSubscription(connection.SocketId, subscriptionId);
|
|
||||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribe all subscriptions
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
|
||||||
{
|
|
||||||
var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
|
||||||
if (sum == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount));
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
{
|
|
||||||
var socketList = socketConnections.Values;
|
|
||||||
foreach (var connection in socketList)
|
|
||||||
{
|
|
||||||
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
|
|
||||||
tasks.Add(connection.CloseAsync(subscription));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reconnect all connections
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task ReconnectAsync()
|
|
||||||
{
|
|
||||||
_logger.ReconnectingAllConnections(socketConnections.Count);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual async Task<CallResult> PrepareConnectionsAsync()
|
|
||||||
{
|
|
||||||
foreach (var item in DedicatedConnectionConfigs)
|
|
||||||
{
|
|
||||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
|
||||||
if (!socketResult)
|
|
||||||
return socketResult.AsDataless();
|
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult(connectResult.Error!);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void SetOptions<T>(UpdateOptions<T> options)
|
|
||||||
{
|
|
||||||
var previousProxyIsSet = ClientOptions.Proxy != null;
|
|
||||||
base.SetOptions(options);
|
|
||||||
|
|
||||||
if ((!previousProxyIsSet && options.Proxy == null)
|
|
||||||
|| socketConnections.IsEmpty)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
|
||||||
|
|
||||||
// Update proxy, also triggers reconnect
|
|
||||||
foreach (var connection in socketConnections)
|
|
||||||
_ = connection.Value.UpdateProxy(options.Proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Log the current state of connections and subscriptions
|
|
||||||
/// </summary>
|
|
||||||
public string GetSubscriptionsState(bool includeSubDetails = true)
|
|
||||||
{
|
|
||||||
return GetState(includeSubDetails).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the state of the client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="includeSubDetails">True to get details for each subscription</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public SocketApiClientState GetState(bool includeSubDetails = true)
|
|
||||||
{
|
|
||||||
var connectionStates = new List<SocketConnection.SocketConnectionState>();
|
|
||||||
foreach (var socketIdAndConnection in socketConnections)
|
|
||||||
{
|
|
||||||
SocketConnection connection = socketIdAndConnection.Value;
|
|
||||||
SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails);
|
|
||||||
connectionStates.Add(connectionState);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the current state of the client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Connections">Number of sockets for this client</param>
|
|
||||||
/// <param name="Subscriptions">Total number of subscriptions</param>
|
|
||||||
/// <param name="DownloadSpeed">Total download speed</param>
|
|
||||||
/// <param name="ConnectionStates">State of each socket connection</param>
|
|
||||||
public record SocketApiClientState(
|
|
||||||
int Connections,
|
|
||||||
int Subscriptions,
|
|
||||||
double DownloadSpeed,
|
|
||||||
List<SocketConnection.SocketConnectionState> ConnectionStates)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Print the state of the client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sb"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual bool PrintMembers(StringBuilder sb)
|
|
||||||
{
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine($"\tTotal connections: {Connections}");
|
|
||||||
sb.AppendLine($"\tTotal subscriptions: {Subscriptions}");
|
|
||||||
sb.AppendLine($"\tDownload speed: {DownloadSpeed} kbps");
|
|
||||||
sb.AppendLine($"\tConnections:");
|
|
||||||
ConnectionStates.ForEach(cs =>
|
|
||||||
{
|
|
||||||
sb.AppendLine($"\t\tId: {cs.Id}");
|
|
||||||
sb.AppendLine($"\t\tAddress: {cs.Address}");
|
|
||||||
sb.AppendLine($"\t\tTotal subscriptions: {cs.Subscriptions}");
|
|
||||||
sb.AppendLine($"\t\tStatus: {cs.Status}");
|
|
||||||
sb.AppendLine($"\t\tAuthenticated: {cs.Authenticated}");
|
|
||||||
sb.AppendLine($"\t\tDownload speed: {cs.DownloadSpeed} kbps");
|
|
||||||
sb.AppendLine($"\t\tPending queries: {cs.PendingQueries}");
|
|
||||||
if (cs.SubscriptionStates?.Count > 0)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"\t\tSubscriptions:");
|
|
||||||
cs.SubscriptionStates.ForEach(subState =>
|
|
||||||
{
|
|
||||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
|
||||||
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
|
||||||
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
|
||||||
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose the client
|
|
||||||
/// </summary>
|
|
||||||
public override void Dispose()
|
|
||||||
{
|
|
||||||
_disposing = true;
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
{
|
|
||||||
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
|
||||||
if (socketList.Any())
|
|
||||||
_logger.DisposingSocketClient();
|
|
||||||
|
|
||||||
foreach (var connection in socketList)
|
|
||||||
{
|
|
||||||
tasks.Add(connection.CloseAsync());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
semaphoreSlim?.Dispose();
|
|
||||||
base.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the listener identifier for the message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="messageAccessor"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Preprocess a stream message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connection"></param>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
CryptoExchange.Net/CommonObjects/Balance.cs
Normal file
21
CryptoExchange.Net/CommonObjects/Balance.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Balance data
|
||||||
|
/// </summary>
|
||||||
|
public class Balance: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The asset name
|
||||||
|
/// </summary>
|
||||||
|
public string Asset { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity available
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Available { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total quantity
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Total { get; set; }
|
||||||
|
}
|
||||||
|
}
|
13
CryptoExchange.Net/CommonObjects/BaseComonObject.cs
Normal file
13
CryptoExchange.Net/CommonObjects/BaseComonObject.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for common objects
|
||||||
|
/// </summary>
|
||||||
|
public class BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The source object the data is derived from
|
||||||
|
/// </summary>
|
||||||
|
public object SourceObject { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
77
CryptoExchange.Net/CommonObjects/Enums.cs
Normal file
77
CryptoExchange.Net/CommonObjects/Enums.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order type
|
||||||
|
/// </summary>
|
||||||
|
public enum CommonOrderType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Limit type
|
||||||
|
/// </summary>
|
||||||
|
Limit,
|
||||||
|
/// <summary>
|
||||||
|
/// Market type
|
||||||
|
/// </summary>
|
||||||
|
Market,
|
||||||
|
/// <summary>
|
||||||
|
/// Other order type
|
||||||
|
/// </summary>
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Order side
|
||||||
|
/// </summary>
|
||||||
|
public enum CommonOrderSide
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Buy order
|
||||||
|
/// </summary>
|
||||||
|
Buy,
|
||||||
|
/// <summary>
|
||||||
|
/// Sell order
|
||||||
|
/// </summary>
|
||||||
|
Sell
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Order status
|
||||||
|
/// </summary>
|
||||||
|
public enum CommonOrderStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// placed and not fully filled order
|
||||||
|
/// </summary>
|
||||||
|
Active,
|
||||||
|
/// <summary>
|
||||||
|
/// canceled order
|
||||||
|
/// </summary>
|
||||||
|
Canceled,
|
||||||
|
/// <summary>
|
||||||
|
/// filled order
|
||||||
|
/// </summary>
|
||||||
|
Filled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position side
|
||||||
|
/// </summary>
|
||||||
|
public enum CommonPositionSide
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Long position
|
||||||
|
/// </summary>
|
||||||
|
Long,
|
||||||
|
/// <summary>
|
||||||
|
/// Short position
|
||||||
|
/// </summary>
|
||||||
|
Short,
|
||||||
|
/// <summary>
|
||||||
|
/// Both
|
||||||
|
/// </summary>
|
||||||
|
Both
|
||||||
|
}
|
||||||
|
}
|
35
CryptoExchange.Net/CommonObjects/Kline.cs
Normal file
35
CryptoExchange.Net/CommonObjects/Kline.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Kline data
|
||||||
|
/// </summary>
|
||||||
|
public class Kline: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Opening time of the kline
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OpenTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Price at the open time
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OpenPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Highest price of the kline
|
||||||
|
/// </summary>
|
||||||
|
public decimal? HighPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Lowest price of the kline
|
||||||
|
/// </summary>
|
||||||
|
public decimal? LowPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Close price of the kline
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ClosePrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Volume of the kline
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Volume { get; set; }
|
||||||
|
}
|
||||||
|
}
|
47
CryptoExchange.Net/CommonObjects/Order.cs
Normal file
47
CryptoExchange.Net/CommonObjects/Order.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order data
|
||||||
|
/// </summary>
|
||||||
|
public class Order: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of the order
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol of the order
|
||||||
|
/// </summary>
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Price of the order
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Price { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity of the order
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Quantity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The quantity of the order which has been filled
|
||||||
|
/// </summary>
|
||||||
|
public decimal? QuantityFilled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Status of the order
|
||||||
|
/// </summary>
|
||||||
|
public CommonOrderStatus Status { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Side of the order
|
||||||
|
/// </summary>
|
||||||
|
public CommonOrderSide Side { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Type of the order
|
||||||
|
/// </summary>
|
||||||
|
public CommonOrderType Type { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Order time
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
|
}
|
21
CryptoExchange.Net/CommonObjects/OrderBook.cs
Normal file
21
CryptoExchange.Net/CommonObjects/OrderBook.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order book data
|
||||||
|
/// </summary>
|
||||||
|
public class OrderBook: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of bids
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
|
||||||
|
/// <summary>
|
||||||
|
/// List of asks
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
|
||||||
|
}
|
||||||
|
}
|
17
CryptoExchange.Net/CommonObjects/OrderBookEntry.cs
Normal file
17
CryptoExchange.Net/CommonObjects/OrderBookEntry.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order book entry
|
||||||
|
/// </summary>
|
||||||
|
public class OrderBookEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity of the entry
|
||||||
|
/// </summary>
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Price of the entry
|
||||||
|
/// </summary>
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
}
|
||||||
|
}
|
17
CryptoExchange.Net/CommonObjects/OrderId.cs
Normal file
17
CryptoExchange.Net/CommonObjects/OrderId.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of an order
|
||||||
|
/// </summary>
|
||||||
|
public class OrderId: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of an order
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
65
CryptoExchange.Net/CommonObjects/Position.cs
Normal file
65
CryptoExchange.Net/CommonObjects/Position.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Position data
|
||||||
|
/// </summary>
|
||||||
|
public class Position: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of the position
|
||||||
|
/// </summary>
|
||||||
|
public string? Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol of the position
|
||||||
|
/// </summary>
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Leverage
|
||||||
|
/// </summary>
|
||||||
|
public decimal Leverage { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Position quantity
|
||||||
|
/// </summary>
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Entry price
|
||||||
|
/// </summary>
|
||||||
|
public decimal? EntryPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Liquidation price
|
||||||
|
/// </summary>
|
||||||
|
public decimal? LiquidationPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Unrealized profit and loss
|
||||||
|
/// </summary>
|
||||||
|
public decimal? UnrealizedPnl { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Realized profit and loss
|
||||||
|
/// </summary>
|
||||||
|
public decimal? RealizedPnl { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Mark price
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MarkPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Auto adding margin
|
||||||
|
/// </summary>
|
||||||
|
public bool? AutoMargin { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Position margin
|
||||||
|
/// </summary>
|
||||||
|
public decimal? PositionMargin { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Position side
|
||||||
|
/// </summary>
|
||||||
|
public CommonPositionSide? Side { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is isolated
|
||||||
|
/// </summary>
|
||||||
|
public bool? Isolated { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Maintenance margin
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MaintananceMargin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
33
CryptoExchange.Net/CommonObjects/Symbol.cs
Normal file
33
CryptoExchange.Net/CommonObjects/Symbol.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol data
|
||||||
|
/// </summary>
|
||||||
|
public class Symbol: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the symbol
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal quantity of an order
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MinTradeQuantity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Step with which the quantity should increase
|
||||||
|
/// </summary>
|
||||||
|
public decimal? QuantityStep { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// step with which the price should increase
|
||||||
|
/// </summary>
|
||||||
|
public decimal? PriceStep { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of decimals for quantity
|
||||||
|
/// </summary>
|
||||||
|
public int? QuantityDecimals { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of decimal for price
|
||||||
|
/// </summary>
|
||||||
|
public int? PriceDecimals { get; set; }
|
||||||
|
}
|
||||||
|
}
|
35
CryptoExchange.Net/CommonObjects/Ticker.cs
Normal file
35
CryptoExchange.Net/CommonObjects/Ticker.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ticker data
|
||||||
|
/// </summary>
|
||||||
|
public class Ticker: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol
|
||||||
|
/// </summary>
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Price 24 hours ago
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Price24H { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Last trade price
|
||||||
|
/// </summary>
|
||||||
|
public decimal? LastPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 24 hour low price
|
||||||
|
/// </summary>
|
||||||
|
public decimal? LowPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 24 hour high price
|
||||||
|
/// </summary>
|
||||||
|
public decimal? HighPrice { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 24 hour volume
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Volume { get; set; }
|
||||||
|
}
|
||||||
|
}
|
50
CryptoExchange.Net/CommonObjects/Trade.cs
Normal file
50
CryptoExchange.Net/CommonObjects/Trade.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.CommonObjects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Trade data
|
||||||
|
/// </summary>
|
||||||
|
public class Trade: BaseCommonObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol of the trade
|
||||||
|
/// </summary>
|
||||||
|
public string Symbol { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Price of the trade
|
||||||
|
/// </summary>
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity of the trade
|
||||||
|
/// </summary>
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp of the trade
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User trade info
|
||||||
|
/// </summary>
|
||||||
|
public class UserTrade: Trade
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of the trade
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Order id of the trade
|
||||||
|
/// </summary>
|
||||||
|
public string? OrderId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Fee of the trade
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Fee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The asset the fee is paid in
|
||||||
|
/// </summary>
|
||||||
|
public string? FeeAsset { get; set; }
|
||||||
|
}
|
||||||
|
}
|
201
CryptoExchange.Net/Converters/ArrayConverter.cs
Normal file
201
CryptoExchange.Net/Converters/ArrayConverter.cs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CryptoExchange.Net.Attributes;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||||
|
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||||
|
/// </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>();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (objectType == typeof(JToken))
|
||||||
|
return JToken.Load(reader);
|
||||||
|
|
||||||
|
var result = Activator.CreateInstance(objectType);
|
||||||
|
var arr = JArray.Load(reader);
|
||||||
|
return ParseObject(arr, result, objectType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ParseObject(JArray arr, object result, Type objectType)
|
||||||
|
{
|
||||||
|
foreach (var property in objectType.GetProperties())
|
||||||
|
{
|
||||||
|
var attribute = GetCustomAttribute<ArrayPropertyAttribute>(property);
|
||||||
|
|
||||||
|
if (attribute == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (attribute.Index >= arr.Count)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (property.PropertyType.BaseType == typeof(Array))
|
||||||
|
{
|
||||||
|
var objType = property.PropertyType.GetElementType();
|
||||||
|
var innerArray = (JArray)arr[attribute.Index];
|
||||||
|
var count = 0;
|
||||||
|
if (innerArray.Count == 0)
|
||||||
|
{
|
||||||
|
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
|
||||||
|
property.SetValue(result, arrayResult);
|
||||||
|
}
|
||||||
|
else if (innerArray[0].Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
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!);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
property.SetValue(result, arrayResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||||
|
var innerObj = Activator.CreateInstance(objType!);
|
||||||
|
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
|
||||||
|
property.SetValue(result, arrayResult);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
|
||||||
|
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(property.PropertyType);
|
||||||
|
|
||||||
|
object? value;
|
||||||
|
if (converterAttribute != null)
|
||||||
|
{
|
||||||
|
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
|
||||||
|
}
|
||||||
|
else if (conversionAttribute != null)
|
||||||
|
{
|
||||||
|
value = arr[attribute.Index].ToObject(property.PropertyType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = arr[attribute.Index];
|
||||||
|
}
|
||||||
|
|
||||||
|
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")))
|
||||||
|
{
|
||||||
|
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||||
|
property.SetValue(result, dec);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
writer.WriteStartArray();
|
||||||
|
var props = value.GetType().GetProperties();
|
||||||
|
var ordered = props.OrderBy(p => GetCustomAttribute<ArrayPropertyAttribute>(p)?.Index);
|
||||||
|
|
||||||
|
var last = -1;
|
||||||
|
foreach (var prop in ordered)
|
||||||
|
{
|
||||||
|
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(prop);
|
||||||
|
if (arrayProp == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (arrayProp.Index == last)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
while (arrayProp.Index != last + 1)
|
||||||
|
{
|
||||||
|
writer.WriteValue((string?)null);
|
||||||
|
last += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
last = arrayProp.Index;
|
||||||
|
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
||||||
|
if (converterAttribute != null)
|
||||||
|
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
|
||||||
|
else if (!IsSimple(prop.PropertyType))
|
||||||
|
serializer.Serialize(writer, prop.GetValue(value));
|
||||||
|
else
|
||||||
|
writer.WriteValue(prop.GetValue(value));
|
||||||
|
}
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||||
|
(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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark property as an index in the array
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ArrayPropertyAttribute: Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The index in the array
|
||||||
|
/// </summary>
|
||||||
|
public int Index { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
public ArrayPropertyAttribute(int index)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Mark property as an index in the array
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class ArrayPropertyAttribute : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The index in the array
|
|
||||||
/// </summary>
|
|
||||||
public int Index { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index"></param>
|
|
||||||
public ArrayPropertyAttribute(int index)
|
|
||||||
{
|
|
||||||
Index = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
98
CryptoExchange.Net/Converters/BaseConverter.cs
Normal file
98
CryptoExchange.Net/Converters/BaseConverter.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for enum converters
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of enum to convert</typeparam>
|
||||||
|
public abstract class BaseConverter<T>: JsonConverter where T: struct
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The enum->string mapping
|
||||||
|
/// </summary>
|
||||||
|
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
||||||
|
private readonly bool quotes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="useQuotes"></param>
|
||||||
|
protected BaseConverter(bool useQuotes)
|
||||||
|
{
|
||||||
|
quotes = useQuotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var stringValue = value == null? null: GetValue((T) value);
|
||||||
|
if (quotes)
|
||||||
|
writer.WriteValue(stringValue);
|
||||||
|
else
|
||||||
|
writer.WriteRawValue(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (reader.Value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var stringValue = reader.Value.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!GetValue(stringValue, out var result))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a string value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public T ReadString(string data)
|
||||||
|
{
|
||||||
|
return Mapping.FirstOrDefault(v => v.Value == data).Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
// Check if it is type, or nullable of type
|
||||||
|
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool GetValue(string value, out T result)
|
||||||
|
{
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
|
{
|
||||||
|
result = mapping.Key;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetValue(T value)
|
||||||
|
{
|
||||||
|
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
198
CryptoExchange.Net/Converters/DateTimeConverter.cs
Normal file
198
CryptoExchange.Net/Converters/DateTimeConverter.cs
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
138
CryptoExchange.Net/Converters/EnumConverter.cs
Normal file
138
CryptoExchange.Net/Converters/EnumConverter.cs
Normal file
@ -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.WriteRawValue(stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Caching for JsonSerializerContext instances
|
|
||||||
/// </summary>
|
|
||||||
public static class JsonSerializerContextCache
|
|
||||||
{
|
|
||||||
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
|
||||||
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
|
|
||||||
{
|
|
||||||
var contextType = typeof(T);
|
|
||||||
if (_cache.TryGetValue(contextType, out var context))
|
|
||||||
return context;
|
|
||||||
|
|
||||||
var instance = new T();
|
|
||||||
_cache[contextType] = instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Node accessor
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct NodeAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Index
|
|
||||||
/// </summary>
|
|
||||||
public int? Index { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Property name
|
|
||||||
/// </summary>
|
|
||||||
public string? Property { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
|
||||||
/// </summary>
|
|
||||||
public int Type { get; }
|
|
||||||
|
|
||||||
private NodeAccessor(int? index, string? property, int type)
|
|
||||||
{
|
|
||||||
Index = index;
|
|
||||||
Property = property;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an int node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a string node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a property name node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message access definition
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct MessagePath : IEnumerable<NodeAccessor>
|
|
||||||
{
|
|
||||||
private readonly List<NodeAccessor> _path;
|
|
||||||
|
|
||||||
internal void Add(NodeAccessor node)
|
|
||||||
{
|
|
||||||
_path.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public MessagePath()
|
|
||||||
{
|
|
||||||
_path = new List<NodeAccessor>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new message path
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Get()
|
|
||||||
{
|
|
||||||
return new MessagePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IEnumerable implementation
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerator<NodeAccessor> GetEnumerator()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _path.Count; i++)
|
|
||||||
yield return _path[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message path extension methods
|
|
||||||
/// </summary>
|
|
||||||
public static class MessagePathExtension
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Add a string node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <param name="propName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Property(this MessagePath path, string propName)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.String(propName));
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a property name node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath PropertyName(this MessagePath path)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.PropertyName());
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a int node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <param name="index"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Index(this MessagePath path, int index)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.Int(index));
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message node type
|
|
||||||
/// </summary>
|
|
||||||
public enum NodeType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Array node
|
|
||||||
/// </summary>
|
|
||||||
Array,
|
|
||||||
/// <summary>
|
|
||||||
/// Object node
|
|
||||||
/// </summary>
|
|
||||||
Object,
|
|
||||||
/// <summary>
|
|
||||||
/// Value node
|
|
||||||
/// </summary>
|
|
||||||
Value
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,236 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
|
||||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
|
||||||
/// </summary>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
|
||||||
#else
|
|
||||||
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteStartArray();
|
|
||||||
|
|
||||||
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
|
||||||
var last = -1;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
|
||||||
if (prop.ArrayProperty.Index == last)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
while (prop.ArrayProperty.Index != last + 1)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
last += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
last = prop.ArrayProperty.Index;
|
|
||||||
|
|
||||||
var objValue = prop.PropertyInfo.GetValue(value);
|
|
||||||
if (objValue == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonSerializerOptions? typeOptions = null;
|
|
||||||
if (prop.JsonConverter != null)
|
|
||||||
{
|
|
||||||
typeOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
|
||||||
PropertyNameCaseInsensitive = false,
|
|
||||||
TypeInfoResolver = options.TypeInfoResolver,
|
|
||||||
};
|
|
||||||
typeOptions.Converters.Add(prop.JsonConverter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
|
|
||||||
{
|
|
||||||
if (prop.TargetType == typeof(string))
|
|
||||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
|
||||||
else if (prop.TargetType == typeof(bool))
|
|
||||||
writer.WriteBooleanValue((bool)objValue);
|
|
||||||
else
|
|
||||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var result = Activator.CreateInstance(typeof(T))!;
|
|
||||||
return (T)ParseObject(ref reader, result, typeof(T), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options)
|
|
||||||
#else
|
|
||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
|
||||||
throw new Exception("Not an array");
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
|
|
||||||
if (!indexAttributes.Any())
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attribute in indexAttributes)
|
|
||||||
{
|
|
||||||
var targetType = attribute.TargetType;
|
|
||||||
object? value = null;
|
|
||||||
if (attribute.JsonConverter != null)
|
|
||||||
{
|
|
||||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
|
|
||||||
{
|
|
||||||
newOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
|
||||||
PropertyNameCaseInsensitive = false,
|
|
||||||
Converters = { attribute.JsonConverter },
|
|
||||||
TypeInfoResolver = options.TypeInfoResolver,
|
|
||||||
};
|
|
||||||
_converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc = JsonDocument.ParseValue(ref reader);
|
|
||||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
|
|
||||||
}
|
|
||||||
else if (attribute.DefaultDeserialization)
|
|
||||||
{
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
JsonTokenType.False => false,
|
|
||||||
JsonTokenType.True => true,
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetDecimal(),
|
|
||||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
|
||||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
|
||||||
attribute.PropertyInfo.SetValue(result, value);
|
|
||||||
else
|
|
||||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
|
||||||
#else
|
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
var attributes = new List<ArrayPropertyInfo>();
|
|
||||||
var properties = typeof(T).GetProperties();
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
|
||||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
|
||||||
if (att == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
|
||||||
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
|
||||||
attributes.Add(new ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
ArrayProperty = att,
|
|
||||||
PropertyInfo = property,
|
|
||||||
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
|
||||||
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
|
||||||
TargetType = targetType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
|
||||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
|
||||||
public JsonConverter? JsonConverter { get; set; }
|
|
||||||
public bool DefaultDeserialization { get; set; }
|
|
||||||
public Type TargetType { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
|
||||||
/// </summary>
|
|
||||||
public class BigDecimalConverter : JsonConverter<decimal>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch(OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return reader.GetDecimal();
|
|
||||||
}
|
|
||||||
catch(FormatException)
|
|
||||||
{
|
|
||||||
// Format issue, assume value is too large
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteNumberValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Bool converter
|
|
||||||
/// </summary>
|
|
||||||
public class BoolConverter : JsonConverterFactory
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BoolConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
|
|
||||||
|
|
||||||
public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.True)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.False)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var value = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
value = value?.ToLowerInvariant().Trim();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
if (typeToConvert == typeof(bool))
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case "true":
|
|
||||||
case "yes":
|
|
||||||
case "y":
|
|
||||||
case "1":
|
|
||||||
case "on":
|
|
||||||
return true;
|
|
||||||
case "false":
|
|
||||||
case "no":
|
|
||||||
case "n":
|
|
||||||
case "0":
|
|
||||||
case "off":
|
|
||||||
case "-1":
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new SerializationException($"Can't convert bool value {value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value is bool boolVal)
|
|
||||||
writer.WriteBooleanValue(boolVal);
|
|
||||||
else
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for comma separated enum values
|
|
||||||
/// </summary>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var str = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(str))
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Date time converter
|
|
||||||
/// </summary>
|
|
||||||
public class DateTimeConverter : JsonConverterFactory
|
|
||||||
{
|
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
|
||||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
|
||||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
|
|
||||||
|
|
||||||
private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
{
|
|
||||||
if (typeToConvert == typeof(DateTime))
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType is JsonTokenType.Number)
|
|
||||||
{
|
|
||||||
var longValue = reader.GetDouble();
|
|
||||||
if (longValue == 0 || longValue < 0)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return ParseFromDouble(longValue);
|
|
||||||
}
|
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
|
||||||
{
|
|
||||||
var stringValue = reader.GetString();
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
|
||||||
|| stringValue == "-1"
|
|
||||||
|| stringValue == "0001-01-01T00:00:00Z"
|
|
||||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseFromString(stringValue!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return reader.GetDateTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dtValue = (DateTime)(object)value;
|
|
||||||
if (dtValue == default)
|
|
||||||
writer.WriteStringValue(default(DateTime));
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a long value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="longValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromDouble(double longValue)
|
|
||||||
{
|
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a string value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stringValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromString(string stringValue)
|
|
||||||
{
|
|
||||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
|
||||||
{
|
|
||||||
// Parse 202303261200 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)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
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: " + stringValue);
|
|
||||||
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: " + stringValue);
|
|
||||||
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 <= 0)
|
|
||||||
return default;
|
|
||||||
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: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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(double 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(double 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalConverter : JsonConverter<decimal?>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
|
||||||
// Infinity returned by the server, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch(OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return reader.GetDecimal();
|
|
||||||
}
|
|
||||||
catch(FormatException)
|
|
||||||
{
|
|
||||||
// Format issue, assume value is too large
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
writer.WriteNullValue();
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing decimal values as string
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
|
||||||
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,289 +0,0 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Static EnumConverter methods
|
|
||||||
/// </summary>
|
|
||||||
public static class EnumConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get the enum value from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">String value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public static T? ParseString<T>(string value) where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
=> EnumConverter<T>.ParseString(value);
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public static string GetString<T>(T enumValue) where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
=> EnumConverter<T>.GetString(enumValue);
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public static string? GetString<T>(T? enumValue) where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
=> EnumConverter<T>.GetString(enumValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
|
||||||
/// </summary>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
|
||||||
#else
|
|
||||||
public class EnumConverter<T>
|
|
||||||
#endif
|
|
||||||
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
|
||||||
{
|
|
||||||
private static List<KeyValuePair<T, string>>? _mapping = null;
|
|
||||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
|
||||||
|
|
||||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
|
||||||
|
|
||||||
internal class NullableEnumConverter : JsonConverter<T?>
|
|
||||||
{
|
|
||||||
private readonly EnumConverter<T> _enumConverter;
|
|
||||||
|
|
||||||
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
|
||||||
{
|
|
||||||
_enumConverter = enumConverter;
|
|
||||||
}
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_enumConverter.Write(writer, value.Value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
|
||||||
if (t == null)
|
|
||||||
{
|
|
||||||
if (warn)
|
|
||||||
{
|
|
||||||
if (isEmptyString)
|
|
||||||
{
|
|
||||||
// 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: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new T(); // return default value
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return t.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
|
|
||||||
{
|
|
||||||
isEmptyString = false;
|
|
||||||
warn = false;
|
|
||||||
var enumType = typeof(T);
|
|
||||||
if (_mapping == null)
|
|
||||||
_mapping = AddMapping();
|
|
||||||
|
|
||||||
var stringValue = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetInt32().ToString(),
|
|
||||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringValue))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue!, out var result))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
{
|
|
||||||
isEmptyString = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We received an enum value but weren't able to parse it.
|
|
||||||
if (!_unknownValuesWarned.Contains(stringValue))
|
|
||||||
{
|
|
||||||
warn = true;
|
|
||||||
_unknownValuesWarned.Add(stringValue!);
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var stringValue = GetString(value);
|
|
||||||
writer.WriteStringValue(stringValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, string value, out T? result)
|
|
||||||
{
|
|
||||||
if (_mapping != null)
|
|
||||||
{
|
|
||||||
// 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));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
{
|
|
||||||
result = mapping.Key;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
|
||||||
{
|
|
||||||
var intValue = int.Parse(value);
|
|
||||||
result = (T)Enum.ToObject(objectType, intValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_unknownValuesWarned.Contains(value))
|
|
||||||
{
|
|
||||||
// Check if it is an known unknown value
|
|
||||||
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If no explicit mapping is found try to parse string
|
|
||||||
result = (T)Enum.Parse(objectType, value, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<KeyValuePair<T, string>> AddMapping()
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<T, string>>();
|
|
||||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
|
||||||
var enumMembers = enumType.GetFields();
|
|
||||||
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<T, string>((T)Enum.Parse(enumType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mapping = mapping;
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString(T? enumValue)
|
|
||||||
{
|
|
||||||
if (_mapping == null)
|
|
||||||
_mapping = AddMapping();
|
|
||||||
|
|
||||||
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the enum value from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">String value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static T? ParseString(string value)
|
|
||||||
{
|
|
||||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
|
||||||
if (_mapping == null)
|
|
||||||
_mapping = AddMapping();
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
return mapping.Key;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If no explicit mapping is found try to parse string
|
|
||||||
return (T)Enum.Parse(type, value, true);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public JsonConverter CreateNullableConverter()
|
|
||||||
{
|
|
||||||
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
|
||||||
return _nullableEnumConverter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing enum values as int
|
|
||||||
/// </summary>
|
|
||||||
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
=> writer.WriteNumberValue((int)(object)value);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
internal interface INullableConverterFactory
|
|
||||||
{
|
|
||||||
JsonConverter CreateNullableConverter();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Int converter
|
|
||||||
/// </summary>
|
|
||||||
public class IntConverter : JsonConverter<int?>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader.GetInt32();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
writer.WriteNullValue();
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Int converter
|
|
||||||
/// </summary>
|
|
||||||
public class LongConverter : JsonConverter<long?>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader.GetInt64();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
writer.WriteNullValue();
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization.Metadata;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
internal class NullableEnumConverterFactory : JsonConverterFactory
|
|
||||||
{
|
|
||||||
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
|
|
||||||
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
|
|
||||||
|
|
||||||
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
|
|
||||||
{
|
|
||||||
_jsonTypeInfoResolver = jsonTypeInfoResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
var b = Nullable.GetUnderlyingType(typeToConvert);
|
|
||||||
if (b == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
|
|
||||||
if (typeInfo == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return typeInfo.Converter is INullableConverterFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
|
|
||||||
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
|
|
||||||
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
|
|
||||||
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
|
|
||||||
|
|
||||||
return nullConverterFactory.CreateNullableConverter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Read string or number as string
|
|
||||||
/// </summary>
|
|
||||||
public class NumberStringConverter : JsonConverter<string?>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.Number)
|
|
||||||
{
|
|
||||||
if (reader.TryGetInt64(out var value))
|
|
||||||
return value.ToString();
|
|
||||||
|
|
||||||
return reader.GetDecimal().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return reader.GetString();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for values which contain a nested json value
|
|
||||||
/// </summary>
|
|
||||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value is null)
|
|
||||||
writer.WriteStringValue("");
|
|
||||||
|
|
||||||
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Replace a value on a string property
|
|
||||||
/// </summary>
|
|
||||||
public abstract class ReplaceConverter : JsonConverter<string>
|
|
||||||
{
|
|
||||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public ReplaceConverter(params string[] replaceSets)
|
|
||||||
{
|
|
||||||
_replacementSets = replaceSets.Select(x =>
|
|
||||||
{
|
|
||||||
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
|
||||||
if (split.Length != 2)
|
|
||||||
throw new ArgumentException("Invalid replacement config");
|
|
||||||
return (split[0], split[1]);
|
|
||||||
}).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
foreach (var set in _replacementSets)
|
|
||||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute to mark a model as json serializable. Used for AOT compilation.
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
|
|
||||||
public class SerializationModelAttribute : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SerializationModelAttribute() { }
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
public SerializationModelAttribute(Type type) { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Serializer options
|
|
||||||
/// </summary>
|
|
||||||
public static class SerializerOptions
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
|
||||||
{
|
|
||||||
if (!_cache.TryGetValue(typeResolver, out var options))
|
|
||||||
{
|
|
||||||
options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
|
||||||
PropertyNameCaseInsensitive = false,
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new DateTimeConverter(),
|
|
||||||
new BoolConverter(),
|
|
||||||
new DecimalConverter(),
|
|
||||||
new IntConverter(),
|
|
||||||
new LongConverter(),
|
|
||||||
new NullableEnumConverterFactory(typeResolver)
|
|
||||||
},
|
|
||||||
TypeInfoResolver = typeResolver,
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var converter in additionalConverters)
|
|
||||||
options.Converters.Add(converter);
|
|
||||||
|
|
||||||
options.TypeInfoResolver = typeResolver;
|
|
||||||
_cache.TryAdd(typeResolver, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
|
|
||||||
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
|
|
||||||
|
|
||||||
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
|
|
||||||
{
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
|
||||||
throw new Exception("");
|
|
||||||
|
|
||||||
reader.Read(); // Start array
|
|
||||||
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
|
||||||
reader.Read();
|
|
||||||
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
|
||||||
reader.Read();
|
|
||||||
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.EndArray)
|
|
||||||
throw new Exception("");
|
|
||||||
|
|
||||||
reader.Read(); // End array
|
|
||||||
|
|
||||||
var result = new T();
|
|
||||||
result.QuantityInBaseAsset = baseQuantity;
|
|
||||||
result.QuantityInQuoteAsset = quoteQuantity;
|
|
||||||
result.QuantityInContracts = contractQuantity;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStartArray();
|
|
||||||
if (value.QuantityInBaseAsset == null)
|
|
||||||
writer.WriteNullValue();
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
|
|
||||||
|
|
||||||
if (value.QuantityInQuoteAsset == null)
|
|
||||||
writer.WriteNullValue();
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
|
|
||||||
|
|
||||||
if (value.QuantityInContracts == null)
|
|
||||||
writer.WriteNullValue();
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.QuantityInContracts.Value);
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
|
|
||||||
{
|
|
||||||
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
|
||||||
throw new Exception("");
|
|
||||||
|
|
||||||
reader.Read(); // Start array
|
|
||||||
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
|
||||||
reader.Read();
|
|
||||||
var baseAsset = reader.GetString()!;
|
|
||||||
reader.Read();
|
|
||||||
var quoteAsset = reader.GetString()!;
|
|
||||||
reader.Read();
|
|
||||||
var timeStr = reader.GetString()!;
|
|
||||||
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.EndArray)
|
|
||||||
throw new Exception("");
|
|
||||||
|
|
||||||
reader.Read(); // End array
|
|
||||||
|
|
||||||
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStartArray();
|
|
||||||
writer.WriteStringValue(value.TradingMode.ToString());
|
|
||||||
writer.WriteStringValue(value.BaseAsset);
|
|
||||||
writer.WriteStringValue(value.QuoteAsset);
|
|
||||||
writer.WriteStringValue(value.DeliverTime?.ToString());
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,376 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json message accessor
|
|
||||||
/// </summary>
|
|
||||||
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The JsonDocument loaded
|
|
||||||
/// </summary>
|
|
||||||
protected JsonDocument? _document;
|
|
||||||
|
|
||||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsJson { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => throw new NotImplementedException();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
_customSerializerOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions);
|
|
||||||
return new CallResult<object>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions);
|
|
||||||
return new CallResult<T>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var info = $"Unknown exception: {ex.Message}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType()
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
return _document.RootElement.ValueKind switch
|
|
||||||
{
|
|
||||||
JsonValueKind.Object => NodeType.Object,
|
|
||||||
JsonValueKind.Array => NodeType.Array,
|
|
||||||
_ => NodeType.Value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var node = GetPathNode(path);
|
|
||||||
if (!node.HasValue)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return node.Value.ValueKind switch
|
|
||||||
{
|
|
||||||
JsonValueKind.Object => NodeType.Object,
|
|
||||||
JsonValueKind.Array => NodeType.Array,
|
|
||||||
_ => NodeType.Value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T? GetValue<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(T) == typeof(string))
|
|
||||||
{
|
|
||||||
if (value.Value.ValueKind == JsonValueKind.Number)
|
|
||||||
return (T)(object)value.Value.GetInt64().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T?[]? GetValues<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
JsonElement? currentToken = _document.RootElement;
|
|
||||||
foreach (var node in path)
|
|
||||||
{
|
|
||||||
if (node.Type == 0)
|
|
||||||
{
|
|
||||||
// Int value
|
|
||||||
var val = node.Index!.Value;
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken.Value[val];
|
|
||||||
}
|
|
||||||
else if (node.Type == 1)
|
|
||||||
{
|
|
||||||
// String value
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
|
|
||||||
return null;
|
|
||||||
currentToken = token;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Property name
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken == null)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
|
||||||
{
|
|
||||||
if (bufferStream && stream is not MemoryStream)
|
|
||||||
{
|
|
||||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
|
||||||
_stream = new MemoryStream();
|
|
||||||
stream.CopyTo(_stream);
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
|
||||||
else if (bufferStream)
|
|
||||||
{
|
|
||||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
|
||||||
_stream = stream;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
|
||||||
IsJson = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString()
|
|
||||||
{
|
|
||||||
if (_stream is null)
|
|
||||||
throw new NullReferenceException("Stream not initialized");
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
|
||||||
return textReader.ReadToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_stream?.Dispose();
|
|
||||||
_stream = null;
|
|
||||||
_document?.Dispose();
|
|
||||||
_document = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
|
|
||||||
{
|
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
_bytes = data;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var firstByte = data.Span[0];
|
|
||||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
|
||||||
{
|
|
||||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new ServerError("Not a json value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
|
||||||
IsJson = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString() =>
|
|
||||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
|
||||||
#if NETSTANDARD2_0
|
|
||||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
|
||||||
#else
|
|
||||||
Encoding.UTF8.GetString(_bytes.Span);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_bytes = null;
|
|
||||||
_document?.Dispose();
|
|
||||||
_document = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.Json.Serialization.Metadata;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
|
||||||
{
|
|
||||||
private readonly JsonSerializerOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
|
||||||
#endif
|
|
||||||
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +1,25 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||||
<PackageVersion>9.1.0</PackageVersion>
|
<PackageVersion>5.1.11</PackageVersion>
|
||||||
<AssemblyVersion>9.1.0</AssemblyVersion>
|
<AssemblyVersion>5.1.11</AssemblyVersion>
|
||||||
<FileVersion>9.1.0</FileVersion>
|
<FileVersion>5.1.11</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
<PackageReleaseNotes>5.1.11 - Added KeepAliveInterval setting, Fixed port not being copied when setting parameters on request, Fixed inconsistent PackageReference casing in csproj</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>9.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
|
||||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
@ -37,25 +27,28 @@
|
|||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup Label="Transitive Client Packages">
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
@ -1,11 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -14,30 +8,6 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ExchangeHelpers
|
public static class ExchangeHelpers
|
||||||
{
|
{
|
||||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
|
||||||
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
|
||||||
|
|
||||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
|
||||||
{
|
|
||||||
{ 1, "F" },
|
|
||||||
{ 2, "G" },
|
|
||||||
{ 3, "H" },
|
|
||||||
{ 4, "J" },
|
|
||||||
{ 5, "K" },
|
|
||||||
{ 6, "M" },
|
|
||||||
{ 7, "N" },
|
|
||||||
{ 8, "Q" },
|
|
||||||
{ 9, "U" },
|
|
||||||
{ 10, "V" },
|
|
||||||
{ 11, "X" },
|
|
||||||
{ 12, "Z" },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
|
||||||
/// </summary>
|
|
||||||
private static int _lastId;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a value between a min and max
|
/// Clamp a value between a min and max
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -73,14 +43,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
var offset = value % step.Value;
|
var offset = value % step.Value;
|
||||||
if(roundingType == RoundingType.Down)
|
if(roundingType == RoundingType.Down)
|
||||||
{
|
|
||||||
value -= offset;
|
value -= offset;
|
||||||
}
|
|
||||||
else if(roundingType == RoundingType.Up)
|
|
||||||
{
|
|
||||||
if (offset != 0)
|
|
||||||
value += (step.Value - offset);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (offset < step / 2)
|
if (offset < step / 2)
|
||||||
@ -112,34 +75,6 @@ namespace CryptoExchange.Net
|
|||||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Apply the provided rules to the value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Value to be adjusted</param>
|
|
||||||
/// <param name="decimals">Max decimal places</param>
|
|
||||||
/// <param name="valueStep">The value step for increase/decrease value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal ApplyRules(
|
|
||||||
decimal value,
|
|
||||||
int? decimals = null,
|
|
||||||
decimal? valueStep = null)
|
|
||||||
{
|
|
||||||
if (valueStep.HasValue)
|
|
||||||
{
|
|
||||||
var offset = value % valueStep.Value;
|
|
||||||
if (offset != 0)
|
|
||||||
{
|
|
||||||
if (offset < valueStep.Value / 2)
|
|
||||||
value -= offset;
|
|
||||||
else value += (valueStep.Value - offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (decimals.HasValue)
|
|
||||||
value = Math.Round(value, decimals.Value);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -161,23 +96,17 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rounds a value down
|
/// Rounds a value down to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="i"></param>
|
||||||
|
/// <param name="decimalPlaces"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public static decimal RoundDown(decimal i, double decimalPlaces)
|
public static decimal RoundDown(decimal i, double decimalPlaces)
|
||||||
{
|
{
|
||||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||||
return Math.Floor(i * power) / power;
|
return Math.Floor(i * power) / power;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rounds a value up
|
|
||||||
/// </summary>
|
|
||||||
public static decimal RoundUp(decimal i, double decimalPlaces)
|
|
||||||
{
|
|
||||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
|
||||||
return Math.Ceiling(i * power) / power;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -187,159 +116,5 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
return value / 1.000000000000000000000000000000000m;
|
return value / 1.000000000000000000000000000000000m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static int NextId() => Interlocked.Increment(ref _lastId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return the last unique id that was generated
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static int LastId() => _lastId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a random string of specified length
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="length">Length of the random string</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string RandomString(int length)
|
|
||||||
{
|
|
||||||
var randomChars = new char[length];
|
|
||||||
|
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
|
||||||
#else
|
|
||||||
var random = new Random();
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return new string(randomChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a random string of specified length
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="length">Length of the random string</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string RandomHexString(int length)
|
|
||||||
{
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
|
|
||||||
#else
|
|
||||||
var randomChars = new char[length * 2];
|
|
||||||
var random = new Random();
|
|
||||||
for (int i = 0; i < length * 2; i++)
|
|
||||||
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
|
|
||||||
return "0x" + new string(randomChars);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a long value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="maxLength">Max character length</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static long RandomLong(int maxLength)
|
|
||||||
{
|
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
|
||||||
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
|
|
||||||
#else
|
|
||||||
var random = new Random();
|
|
||||||
var value = random.Next(0, int.MaxValue);
|
|
||||||
#endif
|
|
||||||
var val = value.ToString();
|
|
||||||
if (val.Length > maxLength)
|
|
||||||
return int.Parse(val.Substring(0, maxLength));
|
|
||||||
else
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a random string of specified length
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">The initial string</param>
|
|
||||||
/// <param name="totalLength">Total length of the resulting string</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AppendRandomString(string source, int totalLength)
|
|
||||||
{
|
|
||||||
if (totalLength < source.Length)
|
|
||||||
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
|
||||||
|
|
||||||
if (totalLength == source.Length)
|
|
||||||
return source;
|
|
||||||
|
|
||||||
return source + RandomString(totalLength - source.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the month representation for futures symbol based on the delivery month
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time">Delivery time</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute multiple requests to retrieve multiple pages of the result set
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of the client</typeparam>
|
|
||||||
/// <typeparam name="U">Type of the request</typeparam>
|
|
||||||
/// <param name="paginatedFunc">The func to execute with each request</param>
|
|
||||||
/// <param name="request">The request parameters</param>
|
|
||||||
/// <param name="ct">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var result = new List<T>();
|
|
||||||
ExchangeWebResult<T[]> batch;
|
|
||||||
INextPageToken? nextPageToken = null;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
|
||||||
yield return batch;
|
|
||||||
if (!batch || ct.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
result.AddRange(batch.Data);
|
|
||||||
nextPageToken = batch.NextPageToken;
|
|
||||||
if (nextPageToken == null)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="symbol">The symbol as retrieved from the exchange</param>
|
|
||||||
/// <param name="quantity">Quantity to trade</param>
|
|
||||||
/// <param name="price">Price to trade at</param>
|
|
||||||
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
|
|
||||||
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
|
|
||||||
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
|
|
||||||
{
|
|
||||||
adjustedPrice = price;
|
|
||||||
adjustedQuantity = quantity;
|
|
||||||
var minNotionalAdjust = false;
|
|
||||||
|
|
||||||
if (price != null)
|
|
||||||
{
|
|
||||||
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
|
||||||
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
|
|
||||||
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
|
|
||||||
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
|
|
||||||
{
|
|
||||||
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
|
|
||||||
minNotionalAdjust = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
|
|
||||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Cache for symbol parsing
|
|
||||||
/// </summary>
|
|
||||||
public static class ExchangeSymbolCache
|
|
||||||
{
|
|
||||||
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the cached symbol data for an exchange
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
|
||||||
/// <param name="updateData">Symbol data</param>
|
|
||||||
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
|
|
||||||
{
|
|
||||||
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
|
||||||
{
|
|
||||||
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
|
||||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a symbol name to a SharedSymbol
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
|
||||||
/// <param name="symbolName">Symbol name</param>
|
|
||||||
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
|
|
||||||
{
|
|
||||||
if (symbolName == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
|
||||||
{
|
|
||||||
DeliverTime = symbolInfo.DeliverTime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExchangeInfo
|
|
||||||
{
|
|
||||||
public DateTime UpdateTime { get; set; }
|
|
||||||
public Dictionary<string, SharedSymbol> Symbols { get; set; }
|
|
||||||
|
|
||||||
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
|
||||||
{
|
|
||||||
UpdateTime = updateTime;
|
|
||||||
Symbols = symbols;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System.Globalization;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Newtonsoft.Json;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using Newtonsoft.Json.Linq;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -32,6 +30,18 @@ namespace CryptoExchange.Net
|
|||||||
parameters.Add(key, value);
|
parameters.Add(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="converter"></param>
|
||||||
|
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value, JsonConverter converter)
|
||||||
|
{
|
||||||
|
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a parameter
|
/// Add a parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -43,6 +53,18 @@ namespace CryptoExchange.Net
|
|||||||
parameters.Add(key, value);
|
parameters.Add(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="converter"></param>
|
||||||
|
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value, JsonConverter converter)
|
||||||
|
{
|
||||||
|
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an optional parameter. Not added if value is null
|
/// Add an optional parameter. Not added if value is null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -55,6 +77,44 @@ namespace CryptoExchange.Net
|
|||||||
parameters.Add(key, value);
|
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, object> parameters, string key, object? value, JsonConverter converter)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Create a query string of the specified parameters
|
/// Create a query string of the specified parameters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -62,30 +122,23 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
||||||
/// <param name="serializationType">How to serialize array parameters</param>
|
/// <param name="serializationType">How to serialize array parameters</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||||
{
|
{
|
||||||
var uriString = string.Empty;
|
var uriString = string.Empty;
|
||||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||||
foreach (var arrayEntry in arraysParameters)
|
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}"))}&";
|
||||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
|
||||||
}
|
|
||||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
|
||||||
{
|
|
||||||
var array = (Array)arrayEntry.Value;
|
|
||||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
|
|
||||||
uriString += "&";
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var array = (Array)arrayEntry.Value;
|
var array = (Array)arrayEntry.Value;
|
||||||
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
|
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
|
||||||
|
uriString += "&";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
|
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
|
||||||
uriString = uriString.TrimEnd('&');
|
uriString = uriString.TrimEnd('&');
|
||||||
return uriString;
|
return uriString;
|
||||||
}
|
}
|
||||||
@ -95,27 +148,135 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parameters"></param>
|
/// <param name="parameters"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string ToFormData(this IDictionary<string, object> parameters)
|
public static string ToFormData(this SortedDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var kvp in parameters)
|
foreach (var kvp in parameters)
|
||||||
{
|
{
|
||||||
if (kvp.Value is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
if (kvp.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
var array = (Array)kvp.Value;
|
||||||
foreach (var value in array)
|
foreach (var value in array)
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
formData.Add(kvp.Key, value.ToString());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||||
|
}
|
||||||
|
return formData.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string the secure string is representing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source secure string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetString(this SecureString source)
|
||||||
{
|
{
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
lock (source)
|
||||||
|
{
|
||||||
|
string result;
|
||||||
|
var length = source.Length;
|
||||||
|
var pointer = IntPtr.Zero;
|
||||||
|
var chars = new char[length];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pointer = Marshal.SecureStringToBSTR(source);
|
||||||
|
Marshal.Copy(pointer, chars, 0, length);
|
||||||
|
|
||||||
|
result = string.Join("", chars);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (pointer != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.ZeroFreeBSTR(pointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData.ToString()!;
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <param name="source"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static SecureString ToSecureString(this string source)
|
||||||
|
{
|
||||||
|
var secureString = new SecureString();
|
||||||
|
foreach (var c in source)
|
||||||
|
secureString.AppendChar(c);
|
||||||
|
secureString.MakeReadOnly();
|
||||||
|
return secureString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// String to JToken
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringData"></param>
|
||||||
|
/// <param name="log"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static JToken? ToJToken(this string stringData, Log? log = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(stringData))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JToken.Parse(stringData);
|
||||||
|
}
|
||||||
|
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) 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) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -127,11 +288,9 @@ namespace CryptoExchange.Net
|
|||||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||||
{
|
{
|
||||||
if (!allowedValues.Contains(value))
|
if (!allowedValues.Contains(value))
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates an int is between two values
|
/// Validates an int is between two values
|
||||||
@ -143,11 +302,9 @@ namespace CryptoExchange.Net
|
|||||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||||
{
|
{
|
||||||
if (value < minValue || value > maxValue)
|
if (value < minValue || value > maxValue)
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a string is not null or empty
|
/// Validates a string is not null or empty
|
||||||
@ -193,16 +350,6 @@ namespace CryptoExchange.Net
|
|||||||
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Format a string to RFC3339/ISO8601 string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dateTime"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToRfc3339String(this DateTime dateTime)
|
|
||||||
{
|
|
||||||
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Format an exception and inner exception to a readable string
|
/// Format an exception and inner exception to a readable string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -247,6 +394,26 @@ namespace CryptoExchange.Net
|
|||||||
return url.TrimEnd('/');
|
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>
|
/// <summary>
|
||||||
/// Create a new uri with the provided parameters as query
|
/// Create a new uri with the provided parameters as query
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -254,7 +421,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="baseUri"></param>
|
/// <param name="baseUri"></param>
|
||||||
/// <param name="arraySerialization"></param>
|
/// <param name="arraySerialization"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||||
{
|
{
|
||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
@ -265,32 +432,13 @@ namespace CryptoExchange.Net
|
|||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
{
|
{
|
||||||
if(parameter.Value.GetType().IsArray)
|
if(parameter.Value.GetType().IsArray)
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
foreach (var item in (object[])parameter.Value)
|
foreach (var item in (object[])parameter.Value)
|
||||||
{
|
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
uriBuilder.Query = httpValueCollection.ToString();
|
||||||
return uriBuilder.Uri;
|
return uriBuilder.Uri;
|
||||||
}
|
}
|
||||||
@ -313,36 +461,18 @@ namespace CryptoExchange.Net
|
|||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
{
|
{
|
||||||
if (parameter.Value.GetType().IsArray)
|
if (parameter.Value.GetType().IsArray)
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
foreach (var item in (object[])parameter.Value)
|
foreach (var item in (object[])parameter.Value)
|
||||||
{
|
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
uriBuilder.Query = httpValueCollection.ToString();
|
||||||
return uriBuilder.Uri;
|
return uriBuilder.Uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add parameter to URI
|
/// Add parameter to URI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -350,7 +480,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="name"></param>
|
/// <param name="name"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||||
{
|
{
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||||
|
|
||||||
@ -362,163 +492,6 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
return ub.Uri;
|
return ub.Uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decompress using GzipStream
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
using var decompressedStream = new MemoryStream();
|
|
||||||
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
|
||||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
|
||||||
: new MemoryStream(data.ToArray());
|
|
||||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
|
||||||
deflateStream.CopyTo(decompressedStream);
|
|
||||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decompress using DeflateStream
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
|
||||||
{
|
|
||||||
var output = new MemoryStream();
|
|
||||||
|
|
||||||
using (var compressStream = new MemoryStream(input.ToArray()))
|
|
||||||
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
|
||||||
decompressor.CopyTo(output);
|
|
||||||
|
|
||||||
output.Position = 0;
|
|
||||||
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the trading mode is linear
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the trading mode is inverse
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the trading mode is perpetual
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the trading mode is delivery
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register rest client interfaces
|
|
||||||
/// </summary>
|
|
||||||
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
|
||||||
{
|
|
||||||
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IAssetsRestClient)client(x)!);
|
|
||||||
if (typeof(IBalanceRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBalanceRestClient)client(x)!);
|
|
||||||
if (typeof(IDepositRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
|
||||||
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
|
||||||
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
|
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
|
||||||
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IRecentTradeRestClient)client(x)!);
|
|
||||||
if (typeof(ITradeHistoryRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITradeHistoryRestClient)client(x)!);
|
|
||||||
if (typeof(IWithdrawalRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
|
|
||||||
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
|
||||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
|
||||||
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
|
||||||
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
|
||||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
|
||||||
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
|
|
||||||
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesOrderRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesOrderRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesSymbolRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesSymbolRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesTickerRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesTickerRestClient)client(x)!);
|
|
||||||
if (typeof(IIndexPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IIndexPriceKlineRestClient)client(x)!);
|
|
||||||
if (typeof(ILeverageRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ILeverageRestClient)client(x)!);
|
|
||||||
if (typeof(IMarkPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IMarkPriceKlineRestClient)client(x)!);
|
|
||||||
if (typeof(IOpenInterestRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOpenInterestRestClient)client(x)!);
|
|
||||||
if (typeof(IPositionHistoryRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
|
||||||
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register socket client interfaces
|
|
||||||
/// </summary>
|
|
||||||
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
|
||||||
{
|
|
||||||
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
|
|
||||||
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
|
||||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
|
||||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
|
||||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITickersSocketClient)client(x)!);
|
|
||||||
if (typeof(ITradeSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITradeSocketClient)client(x)!);
|
|
||||||
if (typeof(IUserTradeSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IUserTradeSocketClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
|
||||||
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionSocketClient)client(x)!);
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.3 KiB |
138
CryptoExchange.Net/Interfaces/CommonClients/IBaseRestClient.cs
Normal file
138
CryptoExchange.Net/Interfaces/CommonClients/IBaseRestClient.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
using CryptoExchange.Net.CommonObjects;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Common rest client endpoints
|
||||||
|
/// </summary>
|
||||||
|
public interface IBaseRestClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the exchange
|
||||||
|
/// </summary>
|
||||||
|
string ExchangeName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should be triggered on order placing
|
||||||
|
/// </summary>
|
||||||
|
event Action<OrderId> OnOrderPlaced;
|
||||||
|
/// <summary>
|
||||||
|
/// Should be triggered on order cancelling
|
||||||
|
/// </summary>
|
||||||
|
event Action<OrderId> OnOrderCanceled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the symbol name based on a base and quote asset
|
||||||
|
/// </summary>
|
||||||
|
/// <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<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<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
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol to retrieve the candles for</param>
|
||||||
|
/// <param name="timespan">The timespan to retrieve the candles for. The supported value are dependent on the exchange</param>
|
||||||
|
/// <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<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<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<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<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <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>
|
||||||
|
/// 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>
|
||||||
|
/// <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>
|
||||||
|
/// 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>
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
}
|
28
CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs
Normal file
28
CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using CryptoExchange.Net.CommonObjects;
|
||||||
|
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Common spot endpoints
|
||||||
|
/// </summary>
|
||||||
|
public interface ISpotClient: IBaseRestClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Place an order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol the order is for</param>
|
||||||
|
/// <param name="side">The side of the order</param>
|
||||||
|
/// <param name="type">The type of the order</param>
|
||||||
|
/// <param name="quantity">The quantity of the order</param>
|
||||||
|
/// <param name="price">The price of the order, only for limit orders</param>
|
||||||
|
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||||
|
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||||
|
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||||
|
/// <returns>The id of the resulting order</returns>
|
||||||
|
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Time provider
|
|
||||||
/// </summary>
|
|
||||||
internal interface IAuthTimeProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get current time
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
DateTime GetTime();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base api client
|
|
||||||
/// </summary>
|
|
||||||
public interface IBaseApiClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base address
|
|
||||||
/// </summary>
|
|
||||||
string BaseAddress { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
|
||||||
/// </summary>
|
|
||||||
bool Authenticated { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Format a base and quote asset to an exchange accepted symbol
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseAsset">The base asset</param>
|
|
||||||
/// <param name="quoteAsset">The quote asset</param>
|
|
||||||
/// <param name="tradingMode">The trading mode</param>
|
|
||||||
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Api credentials type</typeparam>
|
|
||||||
/// <param name="options">Options to set</param>
|
|
||||||
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Client for accessing REST API's for different exchanges
|
|
||||||
/// </summary>
|
|
||||||
public interface ICryptoRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Try get
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <returns></returns>
|
|
||||||
T TryGet<T>(Func<T> createFunc);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Client for accessing Websocket API's for different exchanges
|
|
||||||
/// </summary>
|
|
||||||
public interface ICryptoSocketClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Try get a client by type for the service collection
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <returns></returns>
|
|
||||||
T TryGet<T>(Func<T> createFunc);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message accessor
|
|
||||||
/// </summary>
|
|
||||||
public interface IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Is this a json message
|
|
||||||
/// </summary>
|
|
||||||
bool IsJson { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Is the original data available for retrieval
|
|
||||||
/// </summary>
|
|
||||||
bool OriginalDataAvailable { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The underlying data object
|
|
||||||
/// </summary>
|
|
||||||
object? Underlying { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Clear internal data structure
|
|
||||||
/// </summary>
|
|
||||||
void Clear();
|
|
||||||
/// <summary>
|
|
||||||
/// Get the type of node
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
NodeType? GetNodeType();
|
|
||||||
/// <summary>
|
|
||||||
/// Get the type of node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Access path</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
NodeType? GetNodeType(MessagePath path);
|
|
||||||
/// <summary>
|
|
||||||
/// Get the value of a path
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
T? GetValue<T>(MessagePath path);
|
|
||||||
/// <summary>
|
|
||||||
/// Get the values of an array
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
T?[]? GetValues<T>(MessagePath path);
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize the message into this type
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize the message into this type
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the original string value
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
string GetOriginalString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public interface IStreamMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Load a stream message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream"></param>
|
|
||||||
/// <param name="bufferStream"></param>
|
|
||||||
Task<CallResult> Read(Stream stream, bool bufferStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public interface IByteMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Load a data message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
CallResult Read(ReadOnlyMemory<byte> data);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message processor
|
|
||||||
/// </summary>
|
|
||||||
public interface IMessageProcessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the processor
|
|
||||||
/// </summary>
|
|
||||||
public int Id { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The identifiers for this processor
|
|
||||||
/// </summary>
|
|
||||||
public HashSet<string> ListenerIdentifiers { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Handle a message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connection"></param>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
|
||||||
/// <summary>
|
|
||||||
/// Get the type the message should be deserialized to
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="messageAccessor"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Type? GetMessageType(IMessageAccessor messageAccessor);
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a message into object of type
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessor"></param>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user