mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 16:06:15 +00:00
Compare commits
376 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3e635cf0fe | ||
|
1425c66c69 | ||
|
fc3b7cc75b | ||
|
2cc2dc6ceb | ||
|
7da8cedf66 | ||
|
2cf10668dd | ||
|
f1342b5ff2 | ||
|
a04b636a11 | ||
|
e4637ad295 | ||
|
3a1e43dabe | ||
|
10da1a7bfe | ||
|
37320ca862 | ||
|
2074a5e26f | ||
|
6b14cdbf06 | ||
|
3d6267da93 | ||
|
8def7f32af | ||
|
ac295de9f6 | ||
|
d412e0895e | ||
|
1f9e2b4fcb | ||
|
b13cff5a95 | ||
|
4c050744ad | ||
|
3b15c35a02 | ||
|
cd78dbf575 | ||
|
a258532d6a | ||
|
d2a87a1069 | ||
|
e07f24ea0a | ||
|
024e8dcfe2 | ||
|
4bb5aae40a | ||
|
dec94678ec | ||
|
1a49fc8251 | ||
|
29b0875960 | ||
|
976ccab1da | ||
|
02bbd37bb6 | ||
|
1bbbec7f2b | ||
|
0262f04913 | ||
|
fd1ec17d72 | ||
|
4bdad7fe0c | ||
|
74f73dc790 | ||
|
0527a8a76e | ||
|
c693eb8c02 | ||
|
3eb28c7fed | ||
|
618c4922b9 | ||
|
c81b15861d | ||
|
4a5832cccd | ||
|
4e47c4cbdf | ||
|
2af1520ecc | ||
|
cf397af3ab | ||
|
a1479705e2 | ||
|
175e23f110 | ||
|
9b7019ded2 | ||
|
7904aa9ba7 | ||
|
3fe6db589f | ||
|
625dccbbe4 | ||
|
e650771d16 | ||
|
3dad28b19d | ||
|
2b9fda985e | ||
|
ff8759409b | ||
|
0d9627c13f | ||
|
0179fd7e2a | ||
|
b8d0b0cf95 | ||
|
73c42bd452 | ||
|
290be7f5e0 | ||
|
0be1bb16e3 | ||
|
8605196390 | ||
|
460dd97537 | ||
|
1ec5984fad | ||
|
8260c2661d | ||
|
591c1dd405 | ||
|
0164cdfcc4 | ||
|
23a6cfff87 | ||
|
fdcdb90a5f | ||
|
0b7107401f | ||
|
06add65354 | ||
|
773d288497 | ||
|
fd4e8da938 | ||
|
271743b669 | ||
|
f4797caf37 | ||
|
62c9769c72 | ||
|
92d7bc1e2e | ||
|
99e4f96f63 | ||
|
94d8afe149 | ||
|
90ad59c63a | ||
|
c2273edfaa | ||
|
236283f4dd | ||
|
b66f12ff75 | ||
|
0403384beb | ||
|
7d7bc35869 | ||
|
48797038be | ||
|
d21792d04c | ||
|
8414e9d94f | ||
|
ab0243445d | ||
|
f2cf70b02f | ||
|
9ff417bba8 | ||
|
6b43d08a4d | ||
|
39bf7fe9b9 | ||
|
b5893c3b60 | ||
|
15657ba683 | ||
|
1aed9f0c67 | ||
|
17f1560310 | ||
|
41de0a3150 | ||
|
3e410be611 | ||
|
be75449e4a | ||
|
b1b05c8f6b | ||
|
a0e588c3de | ||
|
9e86a08327 | ||
|
ed007b5272 | ||
|
bdd5526244 | ||
|
ce35e30688 | ||
|
b40f72b1b0 | ||
|
31a6cf285b | ||
|
1842f4fda0 | ||
|
7a58902ab6 | ||
|
3cb91296ca | ||
|
130ed40580 | ||
|
94cb2caf0b | ||
|
917d060827 | ||
|
c58bc2be07 | ||
|
ff3356e2b4 | ||
|
79434c7be5 | ||
|
168dabc11f | ||
|
71ee263683 | ||
|
7239b9c289 | ||
|
84d36544e4 | ||
|
a71f57ae7f | ||
|
6e5bcd5e9a | ||
|
4131e563c3 | ||
|
613766dbca | ||
|
23b07d709e | ||
|
bbbdac2fd3 | ||
|
c614b7869c | ||
|
1f31e4a9d7 | ||
|
6cb6cd6b11 | ||
|
17ffec329f | ||
|
7a3927ef49 | ||
|
c1b0437c93 | ||
|
23e947f258 | ||
|
b8686d60b9 | ||
|
5d3de52da6 | ||
|
fee18fd183 | ||
|
3a43d461a3 | ||
|
5f409efad3 | ||
|
cc1f0796fe | ||
|
b1cd9b5412 | ||
|
42003a0247 | ||
|
d89c2bde94 | ||
|
3e6bdaafc6 | ||
|
93e4722a81 | ||
|
355ecb03da | ||
|
994c527c1d | ||
|
69b2e2045e | ||
|
7fde8bf5da | ||
|
637070a7ae | ||
|
7be75f72a7 | ||
|
e3fece41f3 | ||
|
87b0c8d7a2 | ||
|
ca9a711f22 | ||
|
27597bc994 | ||
|
776d75170d | ||
|
949780a9ad | ||
|
2f64cd9f05 | ||
|
185dfeb6fb | ||
|
68067d6258 | ||
|
b309deb0c4 | ||
|
e1dafdf0dd | ||
|
fd7b5f0f0f | ||
|
3b735d66fd | ||
|
3cd505ac8b | ||
|
81d856d78d | ||
|
11c1ad871a | ||
|
ffcb7db8ff | ||
|
02432e5109 | ||
|
a85bfb4432 | ||
|
17d85fdd85 | ||
|
5e0733d7f4 | ||
|
28d5287bd4 | ||
|
ef5097589a | ||
|
f287ec1fa4 | ||
|
28da93af9d | ||
|
0d5bdf5095 | ||
|
8dac3d7aa6 | ||
|
6951f31be7 | ||
|
630f85ec49 | ||
|
9ec4f2276f | ||
|
0a0c66541e | ||
|
bb4199620e | ||
|
8a83cd2cb8 | ||
|
fcfeaf568f | ||
|
25567ea434 | ||
|
1ab85d4c26 | ||
|
be68115099 | ||
|
ff0550b0fb | ||
|
1ab1e008fc | ||
|
6f30c72608 | ||
|
e927bc3d20 | ||
|
09ed7d1436 | ||
|
6fed657ea6 | ||
|
1555f8da0c | ||
|
68b28fc875 | ||
|
5d50d8cde8 | ||
|
9ff673d8be | ||
|
3e5a34fb56 | ||
|
64ee50d98c | ||
|
6a105c6f8f | ||
|
287aadc720 | ||
|
7229438a0b | ||
|
444af98a15 | ||
|
70c6fa1bbb | ||
|
d27f394b46 | ||
|
c8c98e13d0 | ||
|
9fcd722991 | ||
|
8080ecccc0 | ||
|
4b6fa9a1b1 | ||
|
0b6dbde7d4 | ||
|
fe4d63ba75 | ||
|
04bd3727ca | ||
|
7e6fcd03c2 | ||
|
fde8d6353b | ||
|
41b996168a | ||
|
b26f8fb900 | ||
|
bdbbc61d86 | ||
|
d64e200f2f | ||
|
71d54e2f9a | ||
|
ba3975993f | ||
|
050286ecd1 | ||
|
96c9a55c48 | ||
|
a20cbb2f1c | ||
|
2e957d7d9e | ||
|
18d0341056 | ||
|
a2bfed2433 | ||
|
67299338a8 | ||
|
971c049c5f | ||
|
bb7ba5ea49 | ||
|
747c986644 | ||
|
d88087c8ac | ||
|
968bdc330e | ||
|
7b49562c1d | ||
|
24ba60da47 | ||
|
ed5a07fbdb | ||
|
3d3a9b88e7 | ||
|
b2f9d5753e | ||
|
de46c7bd1d | ||
|
5b11d94f73 | ||
|
6f915a3739 | ||
|
d5c4b1bd01 | ||
|
1b1961db00 | ||
|
2dbd5be924 | ||
|
24c40d2dc6 | ||
|
5ef6feb996 | ||
|
85dad6f6f0 | ||
|
3cdcf0d9be | ||
|
b90a0a71e9 | ||
|
e62786a70f | ||
|
87722f2d28 | ||
|
a86276f18d | ||
|
f432a66016 | ||
|
9e2910d2ec | ||
|
46fbc1eb85 | ||
|
f397d3ab94 | ||
|
81a2da1f3f | ||
|
af3303c7b8 | ||
|
8ddd9ecf22 | ||
|
de72fe4fb9 | ||
|
108c8fc183 | ||
|
e86713e949 | ||
|
db9fba4cf2 | ||
|
926802d953 | ||
|
87f5e12b60 | ||
|
034eb83bae | ||
|
7f29275851 | ||
|
2fb3442800 | ||
|
462c857bba | ||
|
61aa589cda | ||
|
27704bf090 | ||
|
4c899861b1 | ||
|
0cff678c2d | ||
|
d18514d73c | ||
|
84736cac3f | ||
|
cda1cce495 | ||
|
fe3a0afd6c | ||
|
d533557324 | ||
|
d91755dff5 | ||
|
27d49a6093 | ||
|
ad1bdd9a3f | ||
|
4f2d7abc7e | ||
|
ef9de5e338 | ||
|
8b513e51b9 | ||
|
d43b38a23a | ||
|
0987c0f9d1 | ||
|
e2dde77023 | ||
|
104ac7caad | ||
|
8788dd3deb | ||
|
f64cc5e9cf | ||
|
75d1bbc6e8 | ||
|
b621aa7e65 | ||
|
9783108695 | ||
|
6ba32fe280 | ||
|
f75cc75bbc | ||
|
2109b65a8e | ||
|
a472751638 | ||
|
f08ed16f2a | ||
|
212d457a6a | ||
|
ac5f333766 | ||
|
640e4387c1 | ||
|
a16b19019f | ||
|
2443f576ac | ||
|
4fd7e44015 | ||
|
a0a3bda1c5 | ||
|
6bda7a3c73 | ||
|
69a7a714cd | ||
|
48e2e6468e | ||
|
4017ac780f | ||
|
a55cd1bb13 | ||
|
b34129e148 | ||
|
be25a68c9c | ||
|
468cd5e48e | ||
|
262c4e4aa5 | ||
|
4ccff6461f | ||
|
3bfa3ef389 | ||
|
5238971bcc | ||
|
2f5c904faf | ||
|
c62775813f | ||
|
f11b3754f0 | ||
|
3cbe0465e9 | ||
|
5048aea722 | ||
|
8d35339ab2 | ||
|
c3316a51e7 | ||
|
7ecf37064b | ||
|
18954f4f53 | ||
|
00bc245102 | ||
|
273cab9fdb | ||
|
84d0f0ec9e | ||
|
690f2a63e5 | ||
|
19cc020852 | ||
|
1d4353e6d1 | ||
|
a02b3f88d7 | ||
|
af44ca4c9f | ||
|
cf2b57bb96 | ||
|
d0a2288910 | ||
|
89c11afc21 | ||
|
da6ed580f1 | ||
|
1c33e297e7 | ||
|
0005534a95 | ||
|
11650f7c1a | ||
|
a222bb3f02 | ||
|
6361c5ef25 | ||
|
892e8a4508 | ||
|
8336d373f3 | ||
|
71072680a8 | ||
|
e13f105019 | ||
|
401577451e | ||
|
5c41ef1ee4 | ||
|
ad614830d1 | ||
|
3365837338 | ||
|
66ac2972d6 | ||
|
0d3e05880a | ||
|
997e71f3b7 | ||
|
b0fca4587d | ||
|
c10671768d | ||
|
91e8123679 | ||
|
417cf2f9ac | ||
|
277be7ab9b | ||
|
45f3459f59 | ||
|
98dad4a8ed | ||
|
1e5f19271b | ||
|
8abeeb4cf0 | ||
|
cae0cd9ead | ||
|
811574ae01 | ||
|
0ddecf7f8d | ||
|
5bcf50fb4d | ||
|
9f0654815d | ||
|
465e9f04f4 | ||
|
7c8cbfa4e2 | ||
|
4c79d13ff9 | ||
|
c815fad135 | ||
|
41f17d0378 | ||
|
50715ff2f7 |
2
.github/workflows/dotnet.yml
vendored
2
.github/workflows/dotnet.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -287,5 +287,3 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||
/Docs/*
|
||||
Docs/
|
||||
|
@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -24,8 +25,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result1 = await waiter1;
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.That(result1);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -39,8 +40,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result1 = await waiter1;
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.That(result1);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -55,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
Assert.That(result1);
|
||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
|
||||
evnt.Set();
|
||||
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result2);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -75,13 +76,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
Assert.That(result1);
|
||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
evnt.Set();
|
||||
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result2);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -105,12 +106,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
for(var i = 1; i <= 10; i++)
|
||||
{
|
||||
evnt.Set();
|
||||
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
await Task.Delay(1); // Wait for the continuation.
|
||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
}
|
||||
|
||||
await resultsWaiter;
|
||||
|
||||
Assert.AreEqual(10, results.Count(r => r));
|
||||
Assert.That(10 == results.Count(r => r));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -124,7 +126,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.That(result1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -134,9 +136,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100));
|
||||
|
||||
var result1 = await waiter1;
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.False(result1);
|
||||
ClassicAssert.False(result1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -11,66 +12,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture()]
|
||||
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]
|
||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
{
|
||||
@ -78,10 +19,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123}");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -91,11 +32,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
@ -108,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||
{
|
||||
var result = baseUrl.AppendPath(path);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -17,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -27,9 +28,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult(null);
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -37,10 +38,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsNull(result.Data);
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
||||
ClassicAssert.IsNull(result.Data);
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -48,10 +49,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult<object>(new object());
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsNotNull(result.Data);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
ClassicAssert.IsNotNull(result.Data);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -60,11 +61,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult.Data is TestObject2);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult.Data is not null);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -73,11 +74,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -86,11 +87,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -99,11 +100,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -111,26 +112,29 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new KeyValuePair<string, string[]>[0],
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new KeyValuePair<string, string[]>[0],
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
Assert.That(asResult.Error.Message == "TestError2");
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -138,25 +142,28 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new KeyValuePair<string, string[]>[0],
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new KeyValuePair<string, string[]>[0],
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,194 +0,0 @@
|
||||
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>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,5 +1,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@ -16,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.ClampValue(min, max, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
||||
@ -33,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
||||
@ -48,7 +50,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(5, 0.1563158, 0.15631)]
|
||||
@ -59,7 +61,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1234560000, "0.123456")]
|
||||
@ -67,7 +69,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void NormalizeTests(decimal input, string expected)
|
||||
{
|
||||
var result = ExchangeHelpers.Normalize(input);
|
||||
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
|
||||
Assert.That(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,8 +1,10 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -34,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException),
|
||||
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -48,248 +50,121 @@ namespace CryptoExchange.Net.UnitTests
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
||||
Assert.That(options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.ApiCredentials.Secret == "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApiOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
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.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
|
||||
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();
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// 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");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
var client = new TestRestClient(options => {
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "333");
|
||||
Assert.That(authProvider2.GetSecret() == "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient();
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient(new TestClientOptions
|
||||
var client = new TestRestClient(options =>
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions()
|
||||
{
|
||||
BaseAddress = "http://test.com"
|
||||
}
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "333");
|
||||
Assert.That(authProvider1.GetSecret() == "444");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
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: BaseRestClientOptions
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </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>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
|
||||
public RestApiClientOptions Api1Options
|
||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
get => _api1Options;
|
||||
set => _api1Options = new RestApiClientOptions(_api1Options, value);
|
||||
}
|
||||
|
||||
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);
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Logging;
|
||||
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
|
||||
{
|
||||
@ -25,14 +26,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
||||
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
|
||||
Assert.That(result.Success);
|
||||
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -43,11 +44,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetResponse("{\"property\": 123", out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -58,11 +59,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||
|
||||
// act
|
||||
var result = await client.Request<TestObject>();
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -73,14 +74,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = await client.Request<TestObject>();
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.IsTrue(result.Error is ServerError);
|
||||
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
|
||||
Assert.IsTrue(result.Error.Message.Contains("123"));
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -91,14 +90,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = await client.Request<TestObject>();
|
||||
var result = await client.Api2.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.IsTrue(result.Error is ServerError);
|
||||
Assert.IsTrue(result.Error.Code == 123);
|
||||
Assert.IsTrue(result.Error.Message == "Invalid request");
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
Assert.That(result.Error.Code == 123);
|
||||
Assert.That(result.Error.Message == "Invalid request");
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -106,23 +105,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||
},
|
||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||
});
|
||||
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
|
||||
options.Api1Options.OutputOriginalData = true;
|
||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@ -136,19 +128,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
}
|
||||
});
|
||||
var client = new TestRestClient();
|
||||
|
||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
||||
|
||||
client.SetResponse("{}", out var request);
|
||||
|
||||
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
||||
{
|
||||
{ "TestParam1", "Value1" },
|
||||
{ "TestParam2", 2 },
|
||||
@ -159,13 +145,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
});
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(request.Method, new HttpMethod(method));
|
||||
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
|
||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
|
||||
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
|
||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
|
||||
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
|
||||
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
||||
Assert.That(request.Method == new HttpMethod(method));
|
||||
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
||||
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
||||
Assert.That(request.GetHeaders().First().Key == "TestHeader");
|
||||
Assert.That(request.GetHeaders().First().Value.Contains("123"));
|
||||
}
|
||||
|
||||
|
||||
@ -175,21 +161,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
var triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests? triggered : !triggered);
|
||||
}
|
||||
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test1", true)]
|
||||
@ -199,35 +186,40 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("/sapi/", "/sapi/", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
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)]
|
||||
@ -236,21 +228,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
bool triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
[TestCase("/", false)]
|
||||
@ -258,17 +251,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,53 +272,41 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
@ -332,35 +314,70 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", true, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, false, true)]
|
||||
[TestCase("/sapi/test", true, true, false, true)]
|
||||
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
|
||||
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
|
||||
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[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,11 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@ -17,19 +23,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
SubOptions = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
},
|
||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
options.SubOptions.MaxSocketConnections = 1;
|
||||
});
|
||||
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6);
|
||||
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@ -42,37 +44,40 @@ namespace CryptoExchange.Net.UnitTests
|
||||
socket.CanConnect = canConnect;
|
||||
|
||||
//act
|
||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket));
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
Assert.That(connectResult.Success == canConnect);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
Dictionary<string, string> result = null;
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.JsonData;
|
||||
result = messageEvent.Data;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
});
|
||||
sub.AddSubscription(subObj);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue((int)result["property"] == 123);
|
||||
Assert.That(result["property"] == "123");
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
@ -80,111 +85,147 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
options.SubOptions.OutputOriginalData = enabled;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
});
|
||||
sub.AddSubscription(subObj);
|
||||
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
socket.InvokeMessage(msgToSend);
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
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);
|
||||
Assert.That(original == (enabled ? msgToSend : null));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
client.ConnectSocketSub(sub);
|
||||
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var ups = new UpdateSubscription(sub, subscription);
|
||||
sub.AddSubscription(subscription);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAsync(ups).Wait();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket.Connected == false);
|
||||
Assert.That(socket.Connected == false);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket1 = client.CreateSocket();
|
||||
var socket2 = client.CreateSocket();
|
||||
socket1.CanConnect = true;
|
||||
socket2.CanConnect = true;
|
||||
var sub1 = new SocketConnection(client, null, socket1);
|
||||
var sub2 = new SocketConnection(client, null, socket2);
|
||||
client.ConnectSocketSub(sub1);
|
||||
client.ConnectSocketSub(sub2);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||
client.SubClient.ConnectSocketSub(sub1);
|
||||
client.SubClient.ConnectSocketSub(sub2);
|
||||
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
|
||||
client.UnsubscribeAllAsync().Wait();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket1.Connected == false);
|
||||
Assert.IsTrue(socket2.Connected == false);
|
||||
Assert.That(socket1.Connected == false);
|
||||
Assert.That(socket2.Connected == false);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
|
||||
// act
|
||||
var connectResult = client.ConnectSocketSub(sub);
|
||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(connectResult.Success);
|
||||
ClassicAssert.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,21 +4,24 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.OrderBook;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SymbolOrderBookTests
|
||||
{
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
|
||||
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base("Test", "BTC/USD", defaultOrderBookOptions)
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(_defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
|
||||
@ -35,12 +38,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
||||
{
|
||||
Status = OrderBookStatus.Synced;
|
||||
base.bids.Clear();
|
||||
base._bids.Clear();
|
||||
foreach (var bid in bids)
|
||||
base.bids.Add(bid.Price, bid);
|
||||
base.asks.Clear();
|
||||
base._bids.Add(bid.Price, bid);
|
||||
base._asks.Clear();
|
||||
foreach (var ask in asks)
|
||||
base.asks.Add(ask.Price, ask);
|
||||
base._asks.Add(ask.Price, ask);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,31 +57,31 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers);
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -101,12 +104,40 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.05m, resultBids.Data);
|
||||
Assert.AreEqual(1.25m, resultAsks.Data);
|
||||
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23333333m, resultAsks2.Data);
|
||||
Assert.That(resultBids.Success);
|
||||
Assert.That(resultAsks.Success);
|
||||
Assert.That(1.05m == resultBids.Data);
|
||||
Assert.That(1.25m == resultAsks.Data);
|
||||
Assert.That(1.06666667m == resultBids2.Data);
|
||||
Assert.That(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
438
CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs
Normal file
438
CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs
Normal file
@ -0,0 +1,438 @@
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
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,31 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
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.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public class TestBaseClient: BaseClient
|
||||
{
|
||||
public TestBaseClient(): base("Test", new BaseClientOptions())
|
||||
public TestSubClient SubClient { get; }
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = new TestClientOptions();
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public void Log(LogLevel verbosity, string data)
|
||||
{
|
||||
log.Write(verbosity, data);
|
||||
_logger.Log(verbosity, data);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubClient : RestApiClient
|
||||
{
|
||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
return Deserialize<T>(data, null, null);
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
||||
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
|
||||
@ -34,16 +77,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
{
|
||||
bodyParameters = new SortedDictionary<string, object>();
|
||||
uriParameters = new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public override string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
public string GetKey() => _credentials.Key;
|
||||
public string GetSecret() => _credentials.Secret;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestObject
|
||||
{
|
||||
[JsonProperty("other")]
|
||||
[JsonPropertyName("other")]
|
||||
public string StringData { get; set; }
|
||||
[JsonPropertyName("intData")]
|
||||
public int IntData { get; set; }
|
||||
[JsonPropertyName("decimalData")]
|
||||
public decimal DecimalData { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@ -12,6 +11,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
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
|
||||
{
|
||||
@ -20,15 +26,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient() : this(new TestClientOptions())
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
Api1 = new TestRestApi1Client(exchangeOptions);
|
||||
Api2 = new TestRestApi2Client(exchangeOptions);
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
Initialize(options.Value);
|
||||
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@ -42,15 +50,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||
var headers = new Dictionary<string, string[]>();
|
||||
var request = new Mock<IRequest>();
|
||||
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.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 List<string> { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
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.GetHeaders()).Returns(() => headers.ToArray());
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
{
|
||||
@ -58,6 +66,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
requestObj = request.Object;
|
||||
}
|
||||
|
||||
@ -68,10 +85,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
||||
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
@ -87,34 +109,46 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||
var headers = new List<KeyValuePair<string, string[]>>();
|
||||
var request = new Mock<IRequest>();
|
||||
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.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);
|
||||
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.GetHeaders()).Returns(headers.ToArray());
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||
{
|
||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
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);
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options)
|
||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", 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)
|
||||
@ -122,7 +156,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public override TimeSpan GetTimeOffset()
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@ -143,12 +177,30 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options)
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||
{
|
||||
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public override TimeSpan GetTimeOffset()
|
||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
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();
|
||||
}
|
||||
@ -167,28 +219,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
}
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
public class TestError
|
||||
{
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
[JsonPropertyName("errorCode")]
|
||||
public int ErrorCode { get; set; }
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
|
||||
protected override Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,125 +1,132 @@
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
//using System;
|
||||
//using System.IO;
|
||||
//using System.Net.WebSockets;
|
||||
//using System.Security.Authentication;
|
||||
//using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
//using CryptoExchange.Net.Interfaces;
|
||||
//using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocket: IWebsocket
|
||||
{
|
||||
public bool CanConnect { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
//{
|
||||
// public class TestSocket: IWebsocket
|
||||
// {
|
||||
// public bool CanConnect { get; set; }
|
||||
// public bool Connected { get; set; }
|
||||
|
||||
public event Action OnClose;
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
// public event Func<Task> OnClose;
|
||||
//#pragma warning disable 0067
|
||||
// public event Func<Task> OnReconnected;
|
||||
// public event Func<Task> OnReconnecting;
|
||||
// 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 bool ShouldReconnect { get; set; }
|
||||
public TimeSpan Timeout { get; set; }
|
||||
public Func<string, string> DataInterpreterString { get; set; }
|
||||
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
public string Url { get; }
|
||||
public bool IsClosed => !Connected;
|
||||
public bool IsOpen => Connected;
|
||||
public bool PingConnection { get; set; }
|
||||
public TimeSpan PingInterval { get; set; }
|
||||
public SslProtocols SSLProtocols { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
// public int Id { get; }
|
||||
// public bool ShouldReconnect { get; set; }
|
||||
// public TimeSpan Timeout { get; set; }
|
||||
// public Func<string, string> DataInterpreterString { get; set; }
|
||||
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
// public DateTime? DisconnectTime { get; set; }
|
||||
// public string Url { get; }
|
||||
// public bool IsClosed => !Connected;
|
||||
// public bool IsOpen => Connected;
|
||||
// public bool PingConnection { get; set; }
|
||||
// public TimeSpan PingInterval { get; set; }
|
||||
// public SslProtocols SSLProtocols { get; set; }
|
||||
// public Encoding Encoding { get; set; }
|
||||
|
||||
public int ConnectCalls { get; private set; }
|
||||
public bool Reconnecting { get; set; }
|
||||
public string Origin { get; set; }
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
// public int ConnectCalls { get; private set; }
|
||||
// public bool Reconnecting { get; set; }
|
||||
// public string Origin { 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 TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
// public static int lastId = 0;
|
||||
// public static object lastIdLock = new object();
|
||||
|
||||
public TestSocket()
|
||||
{
|
||||
lock (lastIdLock)
|
||||
{
|
||||
Id = lastId + 1;
|
||||
lastId++;
|
||||
}
|
||||
}
|
||||
// public TestSocket()
|
||||
// {
|
||||
// lock (lastIdLock)
|
||||
// {
|
||||
// Id = lastId + 1;
|
||||
// lastId++;
|
||||
// }
|
||||
// }
|
||||
|
||||
public Task<bool> ConnectAsync()
|
||||
{
|
||||
Connected = CanConnect;
|
||||
ConnectCalls++;
|
||||
if (CanConnect)
|
||||
InvokeOpen();
|
||||
return Task.FromResult(CanConnect);
|
||||
}
|
||||
// public Task<CallResult> ConnectAsync()
|
||||
// {
|
||||
// Connected = CanConnect;
|
||||
// ConnectCalls++;
|
||||
// if (CanConnect)
|
||||
// InvokeOpen();
|
||||
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
// }
|
||||
|
||||
public void Send(string data)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
}
|
||||
// public bool Send(int requestId, string data, int weight)
|
||||
// {
|
||||
// if(!Connected)
|
||||
// throw new Exception("Socket not connected");
|
||||
// OnRequestSent?.Invoke(requestId);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
// public void Reset()
|
||||
// {
|
||||
// }
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
OnClose?.Invoke();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
// public Task CloseAsync()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// OnClose?.Invoke();
|
||||
// return Task.FromResult(0);
|
||||
// }
|
||||
|
||||
public void SetProxy(string host, int port)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
// public void SetProxy(string host, int port)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
// public void Dispose()
|
||||
// {
|
||||
// }
|
||||
|
||||
public void InvokeClose()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
// public void InvokeClose()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// Reconnecting = true;
|
||||
// OnClose?.Invoke();
|
||||
// }
|
||||
|
||||
public void InvokeOpen()
|
||||
{
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
// public void InvokeOpen()
|
||||
// {
|
||||
// OnOpen?.Invoke();
|
||||
// }
|
||||
|
||||
public void InvokeMessage(string data)
|
||||
{
|
||||
OnMessage?.Invoke(data);
|
||||
}
|
||||
// public void InvokeMessage(string data)
|
||||
// {
|
||||
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
||||
// }
|
||||
|
||||
public void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
// public void SetProxy(ApiProxy proxy)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
public void InvokeError(Exception error)
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync()
|
||||
{
|
||||
while (Connected)
|
||||
await Task.Delay(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
// public void InvokeError(Exception error)
|
||||
// {
|
||||
// OnError?.Invoke(error);
|
||||
// }
|
||||
// public Task ReconnectAsync() => Task.CompletedTask;
|
||||
// }
|
||||
//}
|
||||
|
@ -1,87 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CryptoExchange.Net.Testing.Implementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
internal class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient() : this(new TestOptions())
|
||||
/// <summary>
|
||||
/// 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(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
Initialize(options.Value);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
||||
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()
|
||||
{
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
return (TestSocket)CreateSocket("123");
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub).Result;
|
||||
}
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
|
||||
out CallResult<object> callResult)
|
||||
{
|
||||
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();
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestOptions: BaseSocketClientOptions
|
||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
||||
{
|
||||
public ApiClientOptions SubOptions { get; set; } = new ApiClientOptions();
|
||||
public static TestSocketOptions Default = new TestSocketOptions
|
||||
{
|
||||
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
|
||||
{
|
||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
||||
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
||||
|
||||
public TestSubSocketClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||
public Subscription TestSubscription { get; private set; } = null;
|
||||
|
||||
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)
|
||||
=> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestStringLogger : ILogger
|
||||
{
|
||||
StringBuilder _builder = new StringBuilder();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
_builder.AppendLine(formatter(state, exception));
|
||||
}
|
||||
|
||||
public string GetLogs()
|
||||
{
|
||||
return _builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
21
CryptoExchange.Net.UnitTests/TestSerializerContext.cs
Normal file
21
CryptoExchange.Net.UnitTests/TestSerializerContext.cs
Normal file
@ -0,0 +1,21 @@
|
||||
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,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -35,6 +37,10 @@ Global
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -42,6 +48,7 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||
|
@ -1 +1,6 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
|
||||
/// <summary>
|
||||
/// Map a enum entry to string values
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,4 @@
|
||||
#if !NETSTANDARD2_1
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
|
@ -1,66 +1,51 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials: IDisposable
|
||||
public class ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key to authenticate requests
|
||||
/// The api key / label to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Key { get; }
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret to authenticate requests
|
||||
/// The api secret or private key to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Secret { get; }
|
||||
public string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key to authenticate requests
|
||||
/// The api passphrase. Not needed on all exchanges
|
||||
/// </summary>
|
||||
public PrivateKey? PrivateKey { get; }
|
||||
public string? Pass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing a private key for authentication
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key used for signing</param>
|
||||
public ApiCredentials(PrivateKey privateKey)
|
||||
{
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
public ApiCredentialsType CredentialType { get; set; }
|
||||
|
||||
/// <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(SecureString key, SecureString secret)
|
||||
{
|
||||
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)
|
||||
/// <param name="key">The api key / label used for identification</param>
|
||||
/// <param name="secret">The api secret or private key used for signing</param>
|
||||
/// <param name="pass">The api pass for the key. Not always needed</param>
|
||||
/// <param name="credentialType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
Pass = pass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -69,61 +54,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
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();
|
||||
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
21
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Credentials type
|
||||
/// </summary>
|
||||
public enum ApiCredentialsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hmac keys credentials
|
||||
/// </summary>
|
||||
Hmac,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in xml format
|
||||
/// </summary>
|
||||
RsaXml,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||
/// </summary>
|
||||
RsaPem
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -14,26 +16,38 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The provided credentials
|
||||
/// </summary>
|
||||
public ApiCredentials Credentials { get; }
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
protected internal readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
if (credentials.Secret == null)
|
||||
if (credentials.Key == null || credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
Credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -42,24 +56,24 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for the request</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="providedParameters">The request parameters</param>
|
||||
/// <param name="auth">If the requests should be authenticated</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||
/// <param name="headers">The headers that should be send with the request</param>
|
||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||
public abstract void AuthenticateRequest(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object> providedParameters,
|
||||
ref IDictionary<string, object>? uriParameters,
|
||||
ref IDictionary<string, object>? bodyParameters,
|
||||
ref Dictionary<string, string>? headers,
|
||||
bool auth,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
out SortedDictionary<string, object> uriParameters,
|
||||
out SortedDictionary<string, object> bodyParameters,
|
||||
out Dictionary<string, string> headers
|
||||
RequestBodyFormat requestBodyFormat
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@ -73,6 +87,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
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>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
@ -83,7 +108,20 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
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>
|
||||
@ -99,6 +137,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
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>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
@ -112,6 +185,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
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>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
@ -125,6 +233,30 @@ namespace CryptoExchange.Net.Authentication
|
||||
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>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
@ -132,9 +264,18 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
@ -145,9 +286,18 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
@ -174,23 +324,80 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// SHA256 sign the data
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string Sign(string toSign)
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
return toSign;
|
||||
using var rsa = CreateRSA();
|
||||
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>
|
||||
/// Sign a byte array
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
public virtual byte[] Sign(byte[] toSign)
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
return toSign;
|
||||
using var rsa = CreateRSA();
|
||||
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>
|
||||
@ -200,10 +407,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
return Convert.ToHexString(buff);
|
||||
#else
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -221,9 +432,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||
protected DateTime GetTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -231,9 +442,48 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key info
|
||||
/// </summary>
|
||||
public class PrivateKey : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The private key
|
||||
/// </summary>
|
||||
public SecureString Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key's pass phrase
|
||||
/// </summary>
|
||||
public SecureString? Passphrase { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the private key is encrypted or not
|
||||
/// </summary>
|
||||
public bool IsEncrypted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(SecureString key, SecureString passphrase)
|
||||
{
|
||||
Key = key;
|
||||
Passphrase = passphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(string key, string passphrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
|
||||
throw new ArgumentException("Key and passphrase can't be null/empty");
|
||||
|
||||
var secureKey = new SecureString();
|
||||
foreach (var c in key)
|
||||
secureKey.AppendChar(c);
|
||||
secureKey.MakeReadOnly();
|
||||
Key = secureKey;
|
||||
|
||||
var securePassphrase = new SecureString();
|
||||
foreach (var c in passphrase)
|
||||
securePassphrase.AppendChar(c);
|
||||
securePassphrase.MakeReadOnly();
|
||||
Passphrase = securePassphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an unencrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(SecureString key)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentException("Key can't be null/empty");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the private key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PrivateKey Copy()
|
||||
{
|
||||
if (Passphrase == null)
|
||||
return new PrivateKey(Key.GetString());
|
||||
else
|
||||
return new PrivateKey(Key.GetString(), Passphrase.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Passphrase?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
53
CryptoExchange.Net/Caching/MemoryCache.cs
Normal file
53
CryptoExchange.Net/Caching/MemoryCache.cs
Normal file
@ -0,0 +1,53 @@
|
||||
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,89 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient: IDisposable
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
private ApiCredentials? _apiCredentials;
|
||||
private AuthenticationProvider? _authenticationProvider;
|
||||
private bool _created;
|
||||
private bool _disposing;
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// If we are disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_created && !_disposing && _apiCredentials != null)
|
||||
{
|
||||
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
|
||||
_created = true;
|
||||
}
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
}
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// The environment this client communicates to
|
||||
/// </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 }
|
||||
};
|
||||
public string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Request body content type
|
||||
/// Output the original string data along with the deserialized object
|
||||
/// </summary>
|
||||
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not we need to manually parse an error instead of relying on the http status code
|
||||
/// Api options
|
||||
/// </summary>
|
||||
public bool manualParseError = false;
|
||||
public ApiOptions ApiOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// Client Options
|
||||
/// </summary>
|
||||
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
public string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// The base address for this API client
|
||||
/// </summary>
|
||||
internal protected string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api client options
|
||||
/// </summary>
|
||||
internal ApiClientOptions Options { get; }
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="apiOptions">Api client options</param>
|
||||
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions)
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
/// <param name="apiOptions">Api options</param>
|
||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||
{
|
||||
Options = apiOptions;
|
||||
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
|
||||
BaseAddress = apiOptions.BaseAddress;
|
||||
_logger = logger;
|
||||
|
||||
ClientOptions = clientOptions;
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
ApiCredentials = apiCredentials?.Copy();
|
||||
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,21 +85,33 @@ namespace CryptoExchange.Net
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials(ApiCredentials credentials)
|
||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
_apiCredentials = credentials?.Copy();
|
||||
_created = false;
|
||||
_authenticationProvider = null;
|
||||
ApiCredentials = credentials?.Copy();
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
_apiCredentials?.Dispose();
|
||||
AuthenticationProvider?.Credentials?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,295 +1,120 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
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>
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
internal string Name { get; }
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
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();
|
||||
protected internal ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
private readonly object _versionLock = new object();
|
||||
private Version _exchangeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public BaseClientOptions ClientOptions { get; }
|
||||
public ExchangeOptions ClientOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseClient(string name, BaseClientOptions options)
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
log = new Log(name);
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
options.OnLoggingChanged += HandleLogConfigChange;
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||
}
|
||||
|
||||
Name = name;
|
||||
|
||||
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
/// <summary>
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register an API client
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
||||
{
|
||||
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
|
||||
if (ClientOptions == null)
|
||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||
|
||||
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
|
||||
ApiClients.Add(apiClient);
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
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>
|
||||
/// Handle a change in the client options log config
|
||||
/// </summary>
|
||||
private void HandleLogConfigChange()
|
||||
{
|
||||
log.UpdateWriters(ClientOptions.LogWriters);
|
||||
log.Level = ClientOptions.LogLevel;
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -297,8 +122,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Disposing client");
|
||||
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
|
||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
|
@ -1,496 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest client
|
||||
/// </summary>
|
||||
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 />
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, 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 syncTask = apiClient.SyncTimeAsync();
|
||||
var timeSyncInfo = apiClient.GetTimeSyncInfo();
|
||||
if (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)
|
||||
{
|
||||
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());
|
||||
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,663 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System.Xml.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for socket client implementations
|
||||
/// </summary>
|
||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
||||
{
|
||||
#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>
|
||||
/// 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>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool disposing;
|
||||
|
||||
/// <summary>
|
||||
/// If true; data which is a response to a query will also be distributed to subscriptions
|
||||
/// 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; }
|
||||
protected bool _disposing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||
}
|
||||
}
|
||||
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => socketConnections.Count;
|
||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
public new BaseSocketClientOptions ClientOptions { get; }
|
||||
|
||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the exchange this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, 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
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 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 (subscription == null)
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (!connection.AddSubscription(subscription))
|
||||
return null;
|
||||
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.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& 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 >= ClientOptions.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -667,24 +49,12 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
|
||||
SocketSubscription? subscription = null;
|
||||
SocketConnection? connection = null;
|
||||
foreach(var socket in socketConnections.Values.ToList())
|
||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
subscription = socket.GetSubscription(subscriptionId);
|
||||
if (subscription != null)
|
||||
{
|
||||
connection = socket;
|
||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||
if (result)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription == null || connection == null)
|
||||
return;
|
||||
|
||||
log.Write(LogLevel.Information, "Closing subscription " + subscriptionId);
|
||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -697,7 +67,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
log.Write(LogLevel.Information, "Closing subscription " + subscription.Id);
|
||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -707,29 +77,56 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Closing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||
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 tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the client
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
disposing = true;
|
||||
periodicEvent?.Set();
|
||||
periodicEvent?.Dispose();
|
||||
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
_ = UnsubscribeAllAsync();
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
var result = new StringBuilder();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
||||
{
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
67
CryptoExchange.Net/Clients/CryptoBaseClient.cs
Normal file
67
CryptoExchange.Net/Clients/CryptoBaseClient.cs
Normal file
@ -0,0 +1,67 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
27
CryptoExchange.Net/Clients/CryptoRestClient.cs
Normal file
27
CryptoExchange.Net/Clients/CryptoRestClient.cs
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
24
CryptoExchange.Net/Clients/CryptoSocketClient.cs
Normal file
24
CryptoExchange.Net/Clients/CryptoSocketClient.cs
Normal file
@ -0,0 +1,24 @@
|
||||
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,74 +1,700 @@
|
||||
using System;
|
||||
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.Tasks;
|
||||
using CryptoExchange.Net.Caching;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
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;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client for interacting with a REST API
|
||||
/// </summary>
|
||||
public abstract class RestApiClient: BaseApiClient
|
||||
public abstract class RestApiClient : BaseApiClient, IRestApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Get time sync info for an API client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract TimeSyncInfo GetTimeSyncInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Get time offset for an API client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract TimeSpan GetTimeOffset();
|
||||
/// <inheritdoc />
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for this client
|
||||
/// Request body content type
|
||||
/// </summary>
|
||||
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
|
||||
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// List of rate limiters
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="httpClient">HttpClient to use</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
|
||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
/// <returns>Server time</returns>
|
||||
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
|
||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
|
||||
/// <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()
|
||||
{
|
||||
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 (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
@ -92,12 +718,17 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
// 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.TimeSyncState.Semaphore.Release();
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||
}
|
||||
|
||||
private bool ShouldCache(RequestDefinition definition)
|
||||
=> ClientOptions.CachingEnabled
|
||||
&& definition.Method == HttpMethod.Get
|
||||
&& !definition.PreventCaching;
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,861 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
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
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base socket API client for interaction with a websocket API
|
||||
/// </summary>
|
||||
public abstract class SocketApiClient : BaseApiClient
|
||||
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
||||
{
|
||||
#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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="logger">log</param>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions 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;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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!;
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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>();
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
25
CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs
Normal file
25
CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
31
CryptoExchange.Net/Converters/JsonSerializerContextCache.cs
Normal file
31
CryptoExchange.Net/Converters/JsonSerializerContextCache.cs
Normal file
@ -0,0 +1,31 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
49
CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs
Normal file
49
CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs
Normal file
@ -0,0 +1,49 @@
|
||||
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); }
|
||||
}
|
||||
}
|
50
CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs
Normal file
50
CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs
Normal file
@ -0,0 +1,50 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
21
CryptoExchange.Net/Converters/MessageParsing/NodeType.cs
Normal file
21
CryptoExchange.Net/Converters/MessageParsing/NodeType.cs
Normal file
@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
}
|
236
CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs
Normal file
236
CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs
Normal file
@ -0,0 +1,236 @@
|
||||
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!;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
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))));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
}
|
289
CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs
Normal file
289
CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs
Normal file
@ -0,0 +1,289 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal interface INullableConverterFactory
|
||||
{
|
||||
JsonConverter CreateNullableConverter();
|
||||
}
|
||||
}
|
40
CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs
Normal file
40
CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs
Normal file
@ -0,0 +1,40 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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) { }
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,376 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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,25 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>5.1.12</PackageVersion>
|
||||
<AssemblyVersion>5.1.12</AssemblyVersion>
|
||||
<FileVersion>5.1.12</FileVersion>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>9.1.0</PackageVersion>
|
||||
<AssemblyVersion>9.1.0</AssemblyVersion>
|
||||
<FileVersion>9.1.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>5.1.12 - Changed time sync so requests no longer wait for it to complete unless it's the first time, Made log client options changable after client creation, Fixed proxy setting not used when reconnecting socket, Changed MaxSocketConnections to a client options, Updated socket reconnection logic</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</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'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
@ -27,28 +37,25 @@
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</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>
|
||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.5" />
|
||||
</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>
|
||||
</Project>
|
@ -1,5 +1,11 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@ -8,6 +14,30 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
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>
|
||||
/// Clamp a value between a min and max
|
||||
/// </summary>
|
||||
@ -43,7 +73,14 @@ namespace CryptoExchange.Net
|
||||
|
||||
var offset = value % step.Value;
|
||||
if(roundingType == RoundingType.Down)
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else if(roundingType == RoundingType.Up)
|
||||
{
|
||||
if (offset != 0)
|
||||
value += (step.Value - offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
@ -75,6 +112,34 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||
/// </summary>
|
||||
@ -96,17 +161,23 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a value down to
|
||||
/// Rounds a value down
|
||||
/// </summary>
|
||||
/// <param name="i"></param>
|
||||
/// <param name="decimalPlaces"></param>
|
||||
/// <returns></returns>
|
||||
public static decimal RoundDown(decimal i, double decimalPlaces)
|
||||
{
|
||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||
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>
|
||||
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
||||
/// </summary>
|
||||
@ -116,5 +187,159 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
70
CryptoExchange.Net/ExchangeSymbolCache.cs
Normal file
70
CryptoExchange.Net/ExchangeSymbolCache.cs
Normal file
@ -0,0 +1,70 @@
|
||||
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,16 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@ -30,18 +32,6 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Add a parameter
|
||||
/// </summary>
|
||||
@ -53,18 +43,6 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
@ -77,44 +55,6 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Create a query string of the specified parameters
|
||||
/// </summary>
|
||||
@ -122,23 +62,30 @@ namespace CryptoExchange.Net
|
||||
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
||||
/// <param name="serializationType">How to serialize array parameters</param>
|
||||
/// <returns></returns>
|
||||
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||
{
|
||||
var uriString = string.Empty;
|
||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||
foreach (var arrayEntry in arraysParameters)
|
||||
{
|
||||
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
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
|
||||
uriString += "&";
|
||||
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
|
||||
}
|
||||
}
|
||||
|
||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
|
||||
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 = uriString.TrimEnd('&');
|
||||
return uriString;
|
||||
}
|
||||
@ -148,135 +95,27 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFormData(this SortedDictionary<string, object> parameters)
|
||||
public static string ToFormData(this IDictionary<string, object> parameters)
|
||||
{
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
if (kvp.Value is null)
|
||||
continue;
|
||||
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
foreach (var value in array)
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
||||
}
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return formData.ToString()!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -288,8 +127,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||
{
|
||||
if (!allowedValues.Contains(value))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -302,8 +143,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||
{
|
||||
if (value < minValue || value > maxValue)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -350,6 +193,16 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Format an exception and inner exception to a readable string
|
||||
/// </summary>
|
||||
@ -394,26 +247,6 @@ namespace CryptoExchange.Net
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
public static string FillPathParameters(this string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
@ -421,7 +254,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
@ -431,14 +264,33 @@ namespace CryptoExchange.Net
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if(parameter.Value.GetType().IsArray)
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
{
|
||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
@ -462,17 +314,35 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
{
|
||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add parameter to URI
|
||||
/// </summary>
|
||||
@ -480,7 +350,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
||||
{
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
@ -492,6 +362,163 @@ namespace CryptoExchange.Net
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
CryptoExchange.Net/Icon/icon.png
Normal file
BIN
CryptoExchange.Net/Icon/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
@ -1,138 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
16
CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs
Normal file
16
CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Time provider
|
||||
/// </summary>
|
||||
internal interface IAuthTimeProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Get current time
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
DateTime GetTime();
|
||||
}
|
||||
}
|
48
CryptoExchange.Net/Interfaces/IBaseApiClient.cs
Normal file
48
CryptoExchange.Net/Interfaces/IBaseApiClient.cs
Normal file
@ -0,0 +1,48 @@
|
||||
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;
|
||||
}
|
||||
}
|
17
CryptoExchange.Net/Interfaces/ICryptoRestClient.cs
Normal file
17
CryptoExchange.Net/Interfaces/ICryptoRestClient.cs
Normal file
@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
||||
}
|
17
CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs
Normal file
17
CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs
Normal file
@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
||||
}
|
101
CryptoExchange.Net/Interfaces/IMessageAccessor.cs
Normal file
101
CryptoExchange.Net/Interfaces/IMessageAccessor.cs
Normal file
@ -0,0 +1,101 @@
|
||||
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);
|
||||
}
|
||||
}
|
44
CryptoExchange.Net/Interfaces/IMessageProcessor.cs
Normal file
44
CryptoExchange.Net/Interfaces/IMessageProcessor.cs
Normal file
@ -0,0 +1,44 @@
|
||||
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