mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-11 16:32:57 +00:00
Compare commits
466 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87dd2d9d40 | |||
| f2b0cb2f0d | |||
| de0a954a91 | |||
| 4a79ce22ec | |||
| 40d480e1fc | |||
| 74e5cf6fc9 | |||
| 2fd3912795 | |||
| ec44307a0c | |||
| ba55705385 | |||
| 2c63a83117 | |||
| 71b1e5e906 | |||
| eaeba6f27e | |||
| 913bdaa855 | |||
| 5aa5790d0a | |||
| a8321e083e | |||
| ce3fa5f186 | |||
| fff70a9c65 | |||
| cff33bb5ac | |||
| 21c8133292 | |||
| 76772e91ba | |||
| 218e0260ce | |||
| 96b3904266 | |||
| bc8faf9822 | |||
| 90c1b89ceb | |||
| 21206ffb25 | |||
| 5942423bfb | |||
| dc4abc42a7 | |||
| c71a81e686 | |||
| 550c0eabf1 | |||
| 28a2a0c7fd | |||
| 7dd1cd5bbd | |||
| a7ff4416bd | |||
| 669d1f7c9e | |||
| 34ee2d3690 | |||
| 005fb7875d | |||
| fa9300ce97 | |||
| fc2d3fc2d2 | |||
| 187ca6a4ef | |||
| 3b2a85d210 | |||
| c512bee825 | |||
| 0943b052b9 | |||
| a896fffdb3 | |||
| 177daf903b | |||
| aa1ebdc4ed | |||
| 38058c4a70 | |||
| a7eb483479 | |||
| c76931a3b4 | |||
| b90b7e9e0c | |||
| beda53d36d | |||
| 0668f669c1 | |||
| 64250e13db | |||
| 451d38d5e7 | |||
| e11e437bbb | |||
| b8a1ad798d | |||
| 4a851c44f2 | |||
| d079796020 | |||
| f125bc88b0 | |||
| f3d535f286 | |||
| 6e8c6feec2 | |||
| 84b0444caf | |||
| 4be986ebe7 | |||
| 21872f818a | |||
| 4e7c45d758 | |||
| 0e55e5f065 | |||
| b1c5cf318a | |||
| 9d94a24862 | |||
| 6cf9684f55 | |||
| 8043a48c49 | |||
| 7d657dd533 | |||
| 1bfdec1484 | |||
| 8d5b6a53f3 | |||
| ad97102e7c | |||
| d8dc121386 | |||
| 10c3868c00 | |||
| 411ba00a82 | |||
| c1b2b62dbc | |||
| 3960cab7a7 | |||
| 34e9447e55 | |||
| 9d3295acc7 | |||
| 995cd3d84c | |||
| 919cdf0075 | |||
| d181c9cfc1 | |||
| 5943142c44 | |||
| dbc430e838 | |||
| 7413d03d31 | |||
| dd60067684 | |||
| 04e4ddf525 | |||
| 99bf6d7c75 | |||
| 99a203933c | |||
| b43d2a2040 | |||
| ba9c406def | |||
| f5f4d50cc9 | |||
| f87506b490 | |||
| f6f9a53ce5 | |||
| 61130ef54e | |||
| e8bcbd59be | |||
| d433ff7475 | |||
| 71957037d0 | |||
| bcdcdbbd4e | |||
| 1ece13f5bc | |||
| da70ba6ec7 | |||
| a832f0e4d4 | |||
| 6ab4d005a0 | |||
| 18dc935038 | |||
| bb3a534f75 | |||
| 649ba370c6 | |||
| 51732c5ce6 | |||
| 0ba7b46680 | |||
| 94dfbb7b9e | |||
| b8b7512b35 | |||
| aba6b773ce | |||
| d88fb0d356 | |||
| b8c6d55156 | |||
| d9a5481db2 | |||
| 6a8bb42c0e | |||
| 2445f001ab | |||
| c84fa9ac32 | |||
| d44a11c44e | |||
| b215cccda4 | |||
| 3eda488361 | |||
| 993a44de35 | |||
| 99465f99a1 | |||
| d42de1fe90 | |||
| d0284c62c0 | |||
| d92f3b7904 | |||
| 3e1b5ada69 | |||
| 6156fb8154 | |||
| f2753aed1e | |||
| e33d826381 | |||
| 364aa4d324 | |||
| 455b332757 | |||
| 876b895645 | |||
| daf7ed9fe6 | |||
| 3e365f83c9 | |||
| 40977ebdbe | |||
| 4a9058fc1c | |||
| dab9a21608 | |||
| a89c222399 | |||
| 1e356d2a45 | |||
| eed794c2cf | |||
| 2f82e2015b | |||
| ad599badb2 | |||
| 1e45c73f1d | |||
| 49c1fda2c1 | |||
| 32a31e464b | |||
| cddb4167e4 | |||
| 65457d8df2 | |||
| 122a6cad43 | |||
| 4c0e841425 | |||
| 92f5839aec | |||
| 30475dae67 | |||
| 3d942bd503 | |||
| f739520e52 | |||
| 0152603ddb | |||
| aa06e0eead | |||
| 2fde9a285e | |||
| b9f6eb6abb | |||
| d77c4354a6 | |||
| 21860ddf85 | |||
| 2cffa22cc2 | |||
| 985ba9bb29 | |||
| 96f23f163d | |||
| 0e7d49991a | |||
| 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 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 10.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -287,5 +287,3 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||
/Docs/*
|
||||
Docs/
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using ProtoBuf;
|
||||
using ProtoBuf.Meta;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.Protobuf
|
||||
{
|
||||
/// <summary>
|
||||
/// System.Text.Json message accessor
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public abstract class ProtobufMessageAccessor<
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
TIntermediateType> : IMessageAccessor
|
||||
#else
|
||||
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
|
||||
#endif
|
||||
{
|
||||
/// <summary>
|
||||
/// The intermediate deserialization object
|
||||
/// </summary>
|
||||
protected TIntermediateType? _intermediateType;
|
||||
/// <summary>
|
||||
/// Runtime type model
|
||||
/// </summary>
|
||||
protected RuntimeTypeModel _model;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => _intermediateType;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufMessageAccessor(RuntimeTypeModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (_intermediateType == null)
|
||||
throw new InvalidOperationException("Data not read");
|
||||
|
||||
object? value = _intermediateType;
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (value == null)
|
||||
break;
|
||||
|
||||
if (step.Type == 0)
|
||||
{
|
||||
// array index
|
||||
}
|
||||
else if (step.Type == 1)
|
||||
{
|
||||
// property value
|
||||
#pragma warning disable IL2075 // Type is already annotated
|
||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
||||
#pragma warning restore
|
||||
}
|
||||
else
|
||||
{
|
||||
// property name
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
var valueType = value.GetType();
|
||||
if (valueType.IsArray)
|
||||
return NodeType.Array;
|
||||
|
||||
if (IsSimple(valueType))
|
||||
return NodeType.Value;
|
||||
|
||||
return NodeType.Object;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (_intermediateType == null)
|
||||
throw new InvalidOperationException("Data not read");
|
||||
|
||||
object? value = _intermediateType;
|
||||
foreach(var step in path)
|
||||
{
|
||||
if (value == null)
|
||||
break;
|
||||
|
||||
if (step.Type == 0)
|
||||
{
|
||||
// array index
|
||||
}
|
||||
else if (step.Type == 1)
|
||||
{
|
||||
// property value
|
||||
#pragma warning disable IL2075 // Type is already annotated
|
||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
||||
#pragma warning restore
|
||||
}
|
||||
else
|
||||
{
|
||||
// property name
|
||||
}
|
||||
}
|
||||
|
||||
return (T?)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T?[]? GetValues<T>(MessagePath path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string GetOriginalString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public abstract CallResult<object> Deserialize(
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null);
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public abstract CallResult<T> Deserialize<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
T>(MessagePath? path = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json stream message accessor
|
||||
/// </summary>
|
||||
public class ProtobufStreamMessageAccessor<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
|
||||
{
|
||||
private Stream? _stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<object> Deserialize(
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize(type, _stream);
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<T> Deserialize<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
T>(MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize<T>(_stream);
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public 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
|
||||
{
|
||||
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
|
||||
IsValid = true;
|
||||
return Task.FromResult(CallResult.SuccessResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + 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;
|
||||
_intermediateType = default;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protobuf byte message accessor
|
||||
/// </summary>
|
||||
public class ProtobufByteMessageAccessor<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<object> Deserialize(
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new MemoryStream(_bytes.ToArray());
|
||||
stream.Position = 0;
|
||||
var result = _model.Deserialize(type, stream);
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
#if NET5_0_OR_GREATER
|
||||
public override CallResult<T> Deserialize<
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
T>(MessagePath? path = null)
|
||||
#else
|
||||
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize<T>(_bytes);
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
try
|
||||
{
|
||||
_intermediateType = _model.Deserialize<TIntermediate>(data);
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + 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;
|
||||
_intermediateType = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using ProtoBuf.Meta;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.Protobuf
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ProtobufMessageSerializer : IByteMessageSerializer
|
||||
{
|
||||
private RuntimeTypeModel _model;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufMessageSerializer(RuntimeTypeModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
#if NET5_0_OR_GREATER
|
||||
public byte[] Serialize<
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
T>(T message)
|
||||
#else
|
||||
public byte[] Serialize<T>(T message)
|
||||
#endif
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
_model.Serialize(memoryStream, message);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>10.0.1</PackageVersion>
|
||||
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||
<FileVersion>10.0.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\CryptoExchange.Net\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>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>CryptoExchange.Net.Protobuf</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
|
||||
<summary>
|
||||
System.Text.Json message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
|
||||
<summary>
|
||||
The intermediate deserialization object
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
|
||||
<summary>
|
||||
Runtime type model
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
|
||||
<summary>
|
||||
System.Text.Json stream message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
|
||||
<summary>
|
||||
Protobuf byte message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
@@ -0,0 +1,52 @@
|
||||
#  CryptoExchange.Net.Proto
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
||||
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 10.0.1 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 10.0.0 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.13.0 - 10 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.12.0 - 03 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.1 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.0 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.10.0 - 15 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.8.0 - 30 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.7.0 - 01 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.6.0 - 25 Aug 2025
|
||||
* Updated CryptoExchange.Net version to 9.6.0
|
||||
|
||||
* Version 9.5.0 - 19 Aug 2025
|
||||
* Updated CryptoExchange.Net version to 9.5.0
|
||||
|
||||
* Version 9.4.0 - 04 Aug 2025
|
||||
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
* Updated protobuf-net package version to 3.2.56
|
||||
|
||||
* Version 9.3.0 - 23 Jul 2025
|
||||
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.2.0 - 14 Jul 2025
|
||||
* Initial release
|
||||
@@ -1,9 +1,10 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -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,21 @@ 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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CancellingWait_Should_ReturnFalse()
|
||||
{
|
||||
var evnt = new AsyncResetEvent(false, true);
|
||||
|
||||
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
ClassicAssert.False(result1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -21,21 +16,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializingInvalidJson_Should_GiveErrorResult()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
@@ -48,7 +29,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,11 +1,11 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -15,11 +15,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
public void TestBasicErrorCallResult()
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError"));
|
||||
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -27,20 +27,20 @@ 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]
|
||||
public void TestCallResultError()
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError"));
|
||||
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsNull(result.Data);
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(result.Data);
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -48,10 +48,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,50 +60,50 @@ 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]
|
||||
public void TestCallResultErrorAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
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.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAsError()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
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.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultErrorAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
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.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -111,27 +111,30 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
HttpVersion.Version11,
|
||||
new HttpResponseMessage().Headers,
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
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.ErrorCode == "TestError2");
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -139,26 +142,29 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
HttpVersion.Version11,
|
||||
new HttpResponseMessage().Headers,
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new HttpRequestMessage().Headers,
|
||||
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>net10.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="18.0.1"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -16,7 +16,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)]
|
||||
@@ -30,10 +30,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
||||
@@ -48,7 +49,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 +60,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 +68,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,14 +1,8 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -49,9 +43,9 @@ 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]
|
||||
@@ -63,10 +57,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||
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]
|
||||
@@ -79,10 +73,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "444");
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "333");
|
||||
Assert.That(authProvider2.GetSecret() == "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -95,10 +89,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
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]
|
||||
@@ -115,11 +113,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "444");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +135,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
@@ -142,12 +152,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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 System.Threading;
|
||||
using NUnit.Framework.Legacy;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using CryptoExchange.Net.RateLimiting.Filters;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -24,14 +25,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.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]
|
||||
@@ -45,8 +46,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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]
|
||||
@@ -60,8 +61,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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]
|
||||
@@ -75,11 +76,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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]
|
||||
@@ -93,11 +92,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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.ErrorCode == "123");
|
||||
Assert.That(result.Error.Message == "Invalid request");
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -106,15 +105,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
|
||||
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
|
||||
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.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(((TestClientOptions)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
|
||||
@@ -134,7 +133,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
client.SetResponse("{}", out var request);
|
||||
|
||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
||||
{
|
||||
{ "TestParam1", "Value1" },
|
||||
{ "TestParam2", 2 },
|
||||
@@ -145,13 +144,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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -161,50 +160,65 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
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(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
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(new TraceLogger(), "/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)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/api/test1", false)]
|
||||
[TestCase("sapi/test1", false)]
|
||||
[TestCase("sapi/test1", true)]
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 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(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
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 rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||
|
||||
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)]
|
||||
@@ -213,18 +227,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
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(new TraceLogger(), "/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(new TraceLogger(), "/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)]
|
||||
@@ -232,14 +250,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
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(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,47 +271,41 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
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(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
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 rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
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 result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
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)]
|
||||
@@ -297,29 +313,70 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
var 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 result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
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", 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 rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
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 result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
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,174 +1,234 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
//using CryptoExchange.Net.Objects;
|
||||
//using CryptoExchange.Net.Objects.Sockets;
|
||||
//using CryptoExchange.Net.Sockets;
|
||||
//using CryptoExchange.Net.Testing.Implementations;
|
||||
//using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
//using Microsoft.Extensions.Logging;
|
||||
//using Moq;
|
||||
//using NUnit.Framework;
|
||||
//using NUnit.Framework.Legacy;
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Net.Sockets;
|
||||
//using System.Text.Json;
|
||||
//using System.Threading;
|
||||
//using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SocketClientTests
|
||||
{
|
||||
[TestCase]
|
||||
public void SettingOptions_Should_ResultInOptionsSet()
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
options.SubOptions.MaxSocketConnections = 1;
|
||||
});
|
||||
//namespace CryptoExchange.Net.UnitTests
|
||||
//{
|
||||
// [TestFixture]
|
||||
// public class SocketClientTests
|
||||
// {
|
||||
// [TestCase]
|
||||
// public void SettingOptions_Should_ResultInOptionsSet()
|
||||
// {
|
||||
// //arrange
|
||||
// //act
|
||||
// var client = new TestSocketClient(options =>
|
||||
// {
|
||||
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
// options.SubOptions.MaxSocketConnections = 1;
|
||||
// });
|
||||
|
||||
// //assert
|
||||
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
// }
|
||||
|
||||
//assert
|
||||
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
}
|
||||
// [TestCase(true)]
|
||||
// [TestCase(false)]
|
||||
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||
// {
|
||||
// //arrange
|
||||
// var client = new TestSocketClient();
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = canConnect;
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||
{
|
||||
//arrange
|
||||
var client = new TestSocketClient();
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = canConnect;
|
||||
// //act
|
||||
// var connectResult = client.SubClient.ConnectSocketSub(
|
||||
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||
|
||||
//act
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||
// //assert
|
||||
// Assert.That(connectResult.Success == canConnect);
|
||||
// }
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
}
|
||||
// [TestCase]
|
||||
// public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options => {
|
||||
// options.ReconnectInterval = TimeSpan.Zero;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// var rstEvent = new ManualResetEvent(false);
|
||||
// Dictionary<string, string> result = null;
|
||||
|
||||
[TestCase]
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
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(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.JsonData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
rstEvent.WaitOne(1000);
|
||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
// {
|
||||
// result = messageEvent.Data;
|
||||
// rstEvent.Set();
|
||||
// });
|
||||
// sub.AddSubscription(subObj);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue((int)result["property"] == 123);
|
||||
}
|
||||
// // act
|
||||
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||
// rstEvent.WaitOne(1000);
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
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(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
{
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
// // assert
|
||||
// Assert.That(result["property"] == "123");
|
||||
// }
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
rstEvent.WaitOne(1000);
|
||||
// [TestCase(false)]
|
||||
// [TestCase(true)]
|
||||
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options =>
|
||||
// {
|
||||
// options.ReconnectInterval = TimeSpan.Zero;
|
||||
// options.SubOptions.OutputOriginalData = enabled;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// var rstEvent = new ManualResetEvent(false);
|
||||
// string original = null;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||
}
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
// {
|
||||
// original = messageEvent.OriginalData;
|
||||
// rstEvent.Set();
|
||||
// });
|
||||
// sub.AddSubscription(subObj);
|
||||
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||
var ups = new UpdateSubscription(sub, us);
|
||||
sub.AddSubscription(us);
|
||||
// // act
|
||||
// socket.InvokeMessage(msgToSend);
|
||||
// rstEvent.WaitOne(1000);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAsync(ups).Wait();
|
||||
// // assert
|
||||
// Assert.That(original == (enabled ? msgToSend : null));
|
||||
// }
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket.Connected == false);
|
||||
}
|
||||
// [TestCase()]
|
||||
// public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options =>
|
||||
// {
|
||||
// options.ReconnectInterval = TimeSpan.Zero;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
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(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 us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
|
||||
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
|
||||
sub1.AddSubscription(us1);
|
||||
sub2.AddSubscription(us2);
|
||||
var ups1 = new UpdateSubscription(sub1, us1);
|
||||
var ups2 = new UpdateSubscription(sub2, us2);
|
||||
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
// var ups = new UpdateSubscription(sub, subscription);
|
||||
// sub.AddSubscription(subscription);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAllAsync().Wait();
|
||||
// // act
|
||||
// client.UnsubscribeAsync(ups).Wait();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket1.Connected == false);
|
||||
Assert.IsTrue(socket2.Connected == false);
|
||||
}
|
||||
// // assert
|
||||
// Assert.That(socket.Connected == false);
|
||||
// }
|
||||
|
||||
[TestCase()]
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
// [TestCase()]
|
||||
// public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
// {
|
||||
// // arrange
|
||||
// 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(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// 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) => { });
|
||||
|
||||
// act
|
||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
// sub1.AddSubscription(subscription1);
|
||||
// sub2.AddSubscription(subscription2);
|
||||
// var ups1 = new UpdateSubscription(sub1, subscription1);
|
||||
// var ups2 = new UpdateSubscription(sub2, subscription2);
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(connectResult.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
// // act
|
||||
// client.UnsubscribeAllAsync().Wait();
|
||||
|
||||
// // assert
|
||||
// Assert.That(socket1.Connected == false);
|
||||
// Assert.That(socket2.Connected == false);
|
||||
// }
|
||||
|
||||
// [TestCase()]
|
||||
// public void FailingToConnectSocket_Should_ReturnError()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = false;
|
||||
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
|
||||
// // act
|
||||
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
||||
// // assert
|
||||
// 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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||
|
||||
// // act
|
||||
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
||||
// await sub;
|
||||
|
||||
// // assert
|
||||
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||
// }
|
||||
|
||||
// [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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||
|
||||
// // 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.Status == SubscriptionStatus.Subscribed);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -5,22 +5,23 @@ 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(null, "Test", "BTC/USD")
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(defaultOrderBookOptions);
|
||||
Initialize(_defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,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]
|
||||
@@ -103,12 +104,12 @@ 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]
|
||||
@@ -131,12 +132,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.9m, resultBids.Data);
|
||||
Assert.AreEqual(1.61538462m, resultAsks.Data);
|
||||
Assert.AreEqual(1.4m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23076923m, resultAsks2.Data);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using CryptoExchange.Net.Converters;
|
||||
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("nan", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[TestCase("1E-2", 0.01)]
|
||||
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
|
||||
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
|
||||
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
public void TestDecimalConverterString(string value, decimal? expected)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
|
||||
}
|
||||
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("1.1", 1.1)]
|
||||
[TestCase("-1.1", -1.1)]
|
||||
[TestCase("null", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[TestCase("1E-2", 0.01)]
|
||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
public void TestDecimalConverterNumber(string value, decimal? expected)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
||||
}
|
||||
|
||||
[Test()]
|
||||
public void TestArrayConverter()
|
||||
{
|
||||
var data = new Test()
|
||||
{
|
||||
Prop1 = 2,
|
||||
Prop2 = null,
|
||||
Prop3 = "123",
|
||||
Prop3Again = "123",
|
||||
Prop4 = null,
|
||||
Prop5 = new Test2
|
||||
{
|
||||
Prop21 = 3,
|
||||
Prop22 = "456"
|
||||
},
|
||||
Prop6 = new Test3
|
||||
{
|
||||
Prop31 = 4,
|
||||
Prop32 = "789"
|
||||
},
|
||||
Prop7 = TestEnum.Two,
|
||||
TestInternal = new Test
|
||||
{
|
||||
Prop1 = 10
|
||||
},
|
||||
Prop8 = new Test3
|
||||
{
|
||||
Prop31 = 5,
|
||||
Prop32 = "101"
|
||||
},
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
TypeInfoResolver = new SerializationContext()
|
||||
};
|
||||
var serialized = JsonSerializer.Serialize(data);
|
||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||
|
||||
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
||||
Assert.That(deserialized.Prop2, Is.Null);
|
||||
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop4, Is.Null);
|
||||
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
||||
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
||||
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
||||
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
|
||||
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
||||
}
|
||||
|
||||
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
||||
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
||||
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
||||
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
||||
{
|
||||
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
||||
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
||||
|
||||
var serialized = JsonSerializer.Serialize(symbol);
|
||||
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
||||
|
||||
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
|
||||
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
||||
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
||||
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
||||
}
|
||||
|
||||
[TestCase(0.1, null, null)]
|
||||
[TestCase(0.1, 0.1, null)]
|
||||
[TestCase(0.1, 0.1, 0.1)]
|
||||
[TestCase(null, 0.1, null)]
|
||||
[TestCase(null, 0.1, 0.1)]
|
||||
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
||||
{
|
||||
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
||||
|
||||
var serialized = JsonSerializer.Serialize(symbol);
|
||||
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
||||
|
||||
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
||||
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
||||
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
||||
}
|
||||
}
|
||||
|
||||
public class STJDecimalObject
|
||||
{
|
||||
[JsonConverter(typeof(DecimalConverter))]
|
||||
[JsonPropertyName("test")]
|
||||
public decimal? Test { get; set; }
|
||||
}
|
||||
|
||||
public class STJTimeObject
|
||||
{
|
||||
[JsonConverter(typeof(DateTimeConverter))]
|
||||
[JsonPropertyName("time")]
|
||||
public DateTime? Time { get; set; }
|
||||
}
|
||||
|
||||
public class STJEnumObject
|
||||
{
|
||||
public TestEnum? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableSTJEnumObject
|
||||
{
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
public class STJBoolObject
|
||||
{
|
||||
public bool? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableSTJBoolObject
|
||||
{
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||
record Test
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop1 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public int? Prop2 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3Again { get; set; }
|
||||
[ArrayProperty(3)]
|
||||
public string Prop4 { get; set; }
|
||||
[ArrayProperty(4)]
|
||||
public Test2 Prop5 { get; set; }
|
||||
[ArrayProperty(5)]
|
||||
public Test3 Prop6 { get; set; }
|
||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||
public TestEnum? Prop7 { get; set; }
|
||||
[ArrayProperty(7)]
|
||||
public Test TestInternal { get; set; }
|
||||
[ArrayProperty(8), JsonConversion]
|
||||
public Test3 Prop8 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
||||
record Test2
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop21 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public string Prop22 { get; set; }
|
||||
}
|
||||
|
||||
record Test3
|
||||
{
|
||||
[JsonPropertyName("prop31")]
|
||||
public int Prop31 { get; set; }
|
||||
[JsonPropertyName("prop32")]
|
||||
public string Prop32 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||
public enum TestEnum
|
||||
{
|
||||
[Map("1")]
|
||||
One,
|
||||
[Map("2")]
|
||||
Two,
|
||||
[Map("three", "3")]
|
||||
Three,
|
||||
Four
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(Test))]
|
||||
[JsonSerializable(typeof(Test2))]
|
||||
[JsonSerializable(typeof(Test3))]
|
||||
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
|
||||
[JsonSerializable(typeof(STJBoolObject))]
|
||||
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
|
||||
[JsonSerializable(typeof(STJEnumObject))]
|
||||
[JsonSerializable(typeof(STJDecimalObject))]
|
||||
[JsonSerializable(typeof(STJTimeObject))]
|
||||
internal partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -16,13 +23,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
var options = new TestClientOptions();
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
@@ -35,37 +44,47 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public class TestSubClient : RestApiClient
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
||||
|
||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset() => null;
|
||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
{
|
||||
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
|
||||
|
||||
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)
|
||||
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
|
||||
{
|
||||
bodyParameters = new SortedDictionary<string, object>();
|
||||
uriParameters = new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key;
|
||||
public string GetSecret() => _credentials.Secret;
|
||||
}
|
||||
|
||||
public override string Sign(string toSign)
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
return toSign;
|
||||
TestAddress = url;
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +11,15 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Net.Http.Headers;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -22,22 +28,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
}
|
||||
Initialize(options.Value);
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@@ -49,19 +50,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
var response = new Mock<IResponse>();
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||
var headers = new HttpRequestMessage().Headers;
|
||||
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.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);
|
||||
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
@@ -69,8 +70,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
@@ -86,16 +87,16 @@ 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 HttpRequestMessage().Headers);
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
@@ -108,42 +109,49 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
var response = new Mock<IResponse>();
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).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(new HttpRequestMessage().Headers);
|
||||
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||
|
||||
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 IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
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, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
||||
}
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
@@ -151,11 +159,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
@@ -163,37 +166,27 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
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 SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||
}
|
||||
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||
{
|
||||
var errorData = ValidateJson(data);
|
||||
|
||||
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
@@ -202,10 +195,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestError
|
||||
{
|
||||
[JsonPropertyName("errorCode")]
|
||||
public int ErrorCode { get; set; }
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
internal class TestRestMessageHandler : JsonRestMessageHandler
|
||||
{
|
||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||
|
||||
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||
{
|
||||
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
||||
|
||||
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
using System;
|
||||
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; }
|
||||
|
||||
public event Action OnClose;
|
||||
|
||||
#pragma warning disable 0067
|
||||
public event Action OnReconnected;
|
||||
public event Action OnReconnecting;
|
||||
#pragma warning restore 0067
|
||||
public event Action<int> OnRequestSent;
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
|
||||
public int Id { get; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
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 double IncomingKbps => throw new NotImplementedException();
|
||||
|
||||
public Uri Uri => new Uri("");
|
||||
|
||||
public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
|
||||
public TestSocket()
|
||||
{
|
||||
lock (lastIdLock)
|
||||
{
|
||||
Id = lastId + 1;
|
||||
lastId++;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> ConnectAsync()
|
||||
{
|
||||
Connected = CanConnect;
|
||||
ConnectCalls++;
|
||||
if (CanConnect)
|
||||
InvokeOpen();
|
||||
return Task.FromResult(CanConnect);
|
||||
}
|
||||
|
||||
public void Send(int requestId, string data, int weight)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
|
||||
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 InvokeClose()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
Reconnecting = true;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
public void InvokeOpen()
|
||||
{
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
|
||||
public void InvokeMessage(string data)
|
||||
{
|
||||
OnMessage?.Invoke(data);
|
||||
}
|
||||
|
||||
public void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InvokeError(Exception error)
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
public Task ReconnectAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
||||
{
|
||||
public static TestSocketOptions Default = new TestSocketOptions
|
||||
{
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal IWebsocket CreateSocketInternal(string address)
|
||||
{
|
||||
return CreateSocket(address);
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub).Result;
|
||||
}
|
||||
|
||||
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -11,7 +11,11 @@ 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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -35,6 +39,14 @@ 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
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -42,6 +54,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}
|
||||
|
||||
@@ -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,79 +1,92 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 api passphrase. Not needed on all exchanges
|
||||
/// </summary>
|
||||
public string? Pass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
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) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <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) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
|
||||
/// <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");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
Pass = pass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and secret generated by the server
|
||||
/// </summary>
|
||||
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
|
||||
{
|
||||
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an RSA private key in PEM format
|
||||
/// </summary>
|
||||
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an RSA private key in XML format
|
||||
/// </summary>
|
||||
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an Ed25519 private key
|
||||
/// </summary>
|
||||
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a key from a file
|
||||
/// </summary>
|
||||
public static string ReadFromFile(string path)
|
||||
{
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using var streamReader = new StreamReader(fileStream);
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,57 +95,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||
/// </summary>
|
||||
RsaPem
|
||||
RsaPem,
|
||||
/// <summary>
|
||||
/// Ed25519 keys credentials
|
||||
/// </summary>
|
||||
Ed25519
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,96 @@
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
#if NET8_0_OR_GREATER
|
||||
using NSec.Cryptography;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for authentication providers
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
public abstract class AuthenticationProvider
|
||||
{
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
/// <summary>
|
||||
/// The supported credential types
|
||||
/// </summary>
|
||||
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
protected readonly ApiCredentials _credentials;
|
||||
protected internal readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// The Ed25519 private key
|
||||
/// </summary>
|
||||
protected Key? Ed25519Key;
|
||||
#endif
|
||||
|
||||
/// <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");
|
||||
|
||||
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
|
||||
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
|
||||
|
||||
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
|
||||
{
|
||||
#if !NET8_0_OR_GREATER
|
||||
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
|
||||
#endif
|
||||
}
|
||||
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
||||
/// Authenticate a REST request
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for the request</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="providedParameters">The request parameters</param>
|
||||
/// <param name="auth">If the requests should be authenticated</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||
/// <param name="headers">The headers that should be send with the request</param>
|
||||
public abstract void AuthenticateRequest(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object> providedParameters,
|
||||
bool auth,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
out SortedDictionary<string, object> uriParameters,
|
||||
out SortedDictionary<string, object> bodyParameters,
|
||||
out Dictionary<string, string> headers
|
||||
);
|
||||
/// <param name="apiClient">The API client sending the request</param>
|
||||
/// <param name="requestConfig">The request configuration</param>
|
||||
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Get an authentication query for a websocket
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The API client sending the request</param>
|
||||
/// <param name="connection">The connection to authenticate</param>
|
||||
/// <param name="context">Optional context required for creating the authentication query</param>
|
||||
public virtual Query? GetAuthenticationQuery(SocketApiClient apiClient, SocketConnection connection, Dictionary<string, object?>? context = null) => null;
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
@@ -74,6 +103,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>
|
||||
@@ -87,6 +127,19 @@ namespace CryptoExchange.Net.Authentication
|
||||
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>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -100,6 +153,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>
|
||||
@@ -113,6 +201,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>
|
||||
@@ -126,6 +249,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>
|
||||
@@ -133,9 +280,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);
|
||||
}
|
||||
|
||||
@@ -146,9 +302,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);
|
||||
}
|
||||
|
||||
@@ -182,12 +347,81 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
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>
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(string data, SignOutputType? outputType = null)
|
||||
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
if (Ed25519Key == null)
|
||||
{
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
var keyBytes = Convert.FromBase64String(key);
|
||||
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
|
||||
}
|
||||
|
||||
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
#else
|
||||
throw new InvalidOperationException();
|
||||
#endif
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!.GetString()
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
@@ -202,37 +436,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||
{
|
||||
// Read from xml private key format
|
||||
rsa.FromXmlString(_credentials.Secret!.GetString());
|
||||
rsa.FromXmlString(_credentials.Secret!);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a byte array
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <returns></returns>
|
||||
public virtual byte[] Sign(byte[] toSign)
|
||||
{
|
||||
return toSign;
|
||||
return rsa;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -241,11 +452,43 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
=> BytesToHexString(new ArraySegment<byte>(buff));
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to hex string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string BytesToHexString(ArraySegment<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>
|
||||
/// Convert a hex encoded string to byte array
|
||||
/// </summary>
|
||||
/// <param name="hexString"></param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] HexToBytesString(string hexString)
|
||||
{
|
||||
if (hexString.StartsWith("0x"))
|
||||
hexString = hexString.Substring(2);
|
||||
|
||||
byte[] bytes = new byte[hexString.Length / 2];
|
||||
for (int i = 0; i < hexString.Length; i += 2)
|
||||
{
|
||||
string hexSubstring = hexString.Substring(i, 2);
|
||||
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -261,27 +504,69 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Get current timestamp including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
{
|
||||
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetRestOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||
if (includeOneSecondOffset)
|
||||
result = result.AddSeconds(-1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current timestamp including the time sync offset from the api client
|
||||
/// </summary>
|
||||
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
{
|
||||
var timestamp = TimeProvider.GetTime();
|
||||
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
|
||||
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||
|
||||
if (includeOneSecondOffset)
|
||||
timestamp = timestamp.AddSeconds(-1);
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||
/// </summary>
|
||||
protected string GetMillisecondTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||
/// </summary>
|
||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||
/// </summary>
|
||||
protected long GetMillisecondTimestampLong(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).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)
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
return stringSerializer.Serialize(value);
|
||||
else
|
||||
return stringSerializer.Serialize(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.Caching
|
||||
{
|
||||
internal class MemoryCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _lock = new Lock();
|
||||
#else
|
||||
private readonly object _lock = new object();
|
||||
#endif
|
||||
|
||||
/// <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,26 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// </summary>
|
||||
protected string? _clientName;
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
@@ -31,42 +28,26 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the client
|
||||
/// </summary>
|
||||
protected internal string ClientName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_clientName != null)
|
||||
return _clientName;
|
||||
|
||||
_clientName = GetType().Name;
|
||||
return _clientName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// </summary>
|
||||
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
|
||||
{
|
||||
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
||||
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Request body content type
|
||||
/// </summary>
|
||||
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not we need to manually parse an error instead of relying on the http status code
|
||||
/// </summary>
|
||||
public bool manualParseError = false;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
public string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
@@ -77,14 +58,11 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
@@ -96,11 +74,16 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of a response code to known error types
|
||||
/// </summary>
|
||||
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</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>
|
||||
@@ -113,12 +96,10 @@ namespace CryptoExchange.Net
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
ApiCredentials = apiCredentials?.Copy();
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,205 +109,36 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get error info for a response code
|
||||
/// </summary>
|
||||
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
||||
|
||||
/// <summary>
|
||||
/// Get error info for a response code
|
||||
/// </summary>
|
||||
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
if (credentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
ApiCredentials = credentials?.Copy();
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data));
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
string? data = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (OutputOriginalData == true)
|
||||
{
|
||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
|
||||
var result = Deserialize<T>(data, serializer, requestId);
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -335,7 +147,6 @@ namespace CryptoExchange.Net
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
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>
|
||||
/// Whether client is disposed
|
||||
/// </summary>
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
@@ -27,7 +53,14 @@ namespace CryptoExchange.Net
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal ILogger _logger;
|
||||
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _versionLock = new Lock();
|
||||
#else
|
||||
private readonly object _versionLock = new object();
|
||||
#endif
|
||||
private Version _exchangeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
@@ -37,14 +70,12 @@ namespace CryptoExchange.Net
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string name)
|
||||
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.
|
||||
{
|
||||
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
Name = name;
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,13 +83,13 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public virtual void Initialize(ExchangeOptions options)
|
||||
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{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,22 +106,32 @@ namespace CryptoExchange.Net
|
||||
/// 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
|
||||
{
|
||||
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>
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
||||
Disposed = true;
|
||||
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest client
|
||||
@@ -19,6 +20,9 @@ namespace CryptoExchange.Net
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
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>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
|
||||
/// <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)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -45,11 +54,11 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
foreach(var socket in ApiClients.OfType<SocketApiClient>())
|
||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||
if (result)
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +72,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -73,10 +82,10 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
tasks.Add(client.UnsubscribeAllAsync());
|
||||
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -86,12 +95,13 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||
_logger.ReconnectingAllConnections(CurrentConnections);
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -101,9 +111,27 @@ namespace CryptoExchange.Net
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach(var client in ApiClients.OfType<SocketApiClient>())
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using System;
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,216 +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 (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
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 (token.Type == JTokenType.Float)
|
||||
value = token.Value<decimal>();
|
||||
}
|
||||
|
||||
if (value is decimal)
|
||||
{
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
var v = value.ToString();
|
||||
if (decimal.TryParse(v, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,148 +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) => GetString(typeof(T), enumValue);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
private static string? GetString(Type objectType, object? enumValue)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteValue(stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.IO;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// REST message handler
|
||||
/// </summary>
|
||||
public interface IRestMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The `accept` HTTP response header for the request
|
||||
/// </summary>
|
||||
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether a seekable stream is required
|
||||
/// </summary>
|
||||
bool RequiresSeekableStream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parse the response when the HTTP response status indicated an error
|
||||
/// </summary>
|
||||
ValueTask<Error> ParseErrorResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Parse the response when the HTTP response status indicated a rate limit error
|
||||
/// </summary>
|
||||
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the response is an error response; if so return the error.<br />
|
||||
/// Note that if the API returns a standard result wrapper, something like this:
|
||||
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
|
||||
/// then the `CheckDeserializedResponse` method should be used for checking the result
|
||||
/// </summary>
|
||||
ValueTask<Error?> CheckForErrorResponse(
|
||||
RequestDefinition request,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize the response stream
|
||||
/// </summary>
|
||||
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
|
||||
Stream responseStream,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the resulting T object indicates an error or not
|
||||
/// </summary>
|
||||
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// WebSocket message handler
|
||||
/// </summary>
|
||||
public interface ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Get an identifier for the message which can be used to determine the type of the message
|
||||
/// </summary>
|
||||
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
|
||||
|
||||
/// <summary>
|
||||
/// Get optional topic filter, for example a symbol name
|
||||
/// </summary>
|
||||
string? GetTopicFilter(object deserializedObject);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize to the provided type
|
||||
/// </summary>
|
||||
object Deserialize(ReadOnlySpan<byte> data, Type type);
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Message type definition
|
||||
/// </summary>
|
||||
public class MessageTypeDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
|
||||
/// </summary>
|
||||
public bool ForceIfFound { get; set; }
|
||||
/// <summary>
|
||||
/// The fields a message needs to contain for this definition
|
||||
/// </summary>
|
||||
public MessageFieldReference[] Fields { get; set; } = [];
|
||||
/// <summary>
|
||||
/// The callback for getting the identifier string
|
||||
/// </summary>
|
||||
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
|
||||
/// <summary>
|
||||
/// The static identifier string to return when this evaluator is matched
|
||||
/// </summary>
|
||||
public string? StaticIdentifier { get; set; }
|
||||
|
||||
internal string? GetMessageType(SearchResult result)
|
||||
{
|
||||
if (StaticIdentifier != null)
|
||||
return StaticIdentifier;
|
||||
|
||||
return TypeIdentifierCallback!(result);
|
||||
}
|
||||
|
||||
internal bool Satisfied(SearchResult result)
|
||||
{
|
||||
foreach(var field in Fields)
|
||||
{
|
||||
if (!result.Contains(field))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
internal class MessageEvalutorFieldReference
|
||||
{
|
||||
public bool SkipReading { get; set; }
|
||||
public bool OverlappingField { get; set; }
|
||||
public MessageFieldReference Field { get; set; }
|
||||
public MessageTypeDefinition? ForceEvaluator { get; set; }
|
||||
|
||||
public MessageEvalutorFieldReference(MessageFieldReference field)
|
||||
{
|
||||
Field = field;
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference to a message field
|
||||
/// </summary>
|
||||
public abstract class MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The name for this search field
|
||||
/// </summary>
|
||||
public string SearchName { get; set; }
|
||||
/// <summary>
|
||||
/// The depth at which to look for this field
|
||||
/// </summary>
|
||||
public int Depth { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// Callback to check if the field value matches an expected constraint
|
||||
/// </summary>
|
||||
public Func<string?, bool>? Constraint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is one of the string values in the set
|
||||
/// </summary>
|
||||
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
|
||||
{
|
||||
Constraint = set.Contains;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is equal to a string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithEqualConstraint(string compare)
|
||||
{
|
||||
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is not equal to a string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithNotEqualConstraint(string compare)
|
||||
{
|
||||
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is not null
|
||||
/// </summary>
|
||||
public MessageFieldReference WithNotNullConstraint()
|
||||
{
|
||||
Constraint = x => x != null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithStartsWithConstraint(string start)
|
||||
{
|
||||
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
|
||||
{
|
||||
Constraint = x =>
|
||||
{
|
||||
if (x == null)
|
||||
return false;
|
||||
|
||||
foreach (var item in startValues)
|
||||
{
|
||||
if (x!.StartsWith(item, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
|
||||
{
|
||||
Constraint = constraint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessageFieldReference(string searchName)
|
||||
{
|
||||
SearchName = searchName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a property message field
|
||||
/// </summary>
|
||||
public class PropertyFieldReference : MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The property name in the JSON
|
||||
/// </summary>
|
||||
public byte[] PropertyName { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the property value is array values
|
||||
/// </summary>
|
||||
public bool ArrayValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PropertyFieldReference(string propertyName) : base(propertyName)
|
||||
{
|
||||
PropertyName = Encoding.UTF8.GetBytes(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to an array message field
|
||||
/// </summary>
|
||||
public class ArrayFieldReference : MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int ArrayIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
|
||||
{
|
||||
Depth = depth;
|
||||
ArrayIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// The results of a search for fields in a JSON message
|
||||
/// </summary>
|
||||
public class SearchResult
|
||||
{
|
||||
private List<SearchResultItem> _items = new List<SearchResultItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a field
|
||||
/// </summary>
|
||||
public string? FieldValue(string searchName)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
throw new Exception($"No field value found for {searchName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of found search field values
|
||||
/// </summary>
|
||||
public int Count => _items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Clear the search result
|
||||
/// </summary>
|
||||
public void Clear() => _items.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the value for a specific field was found
|
||||
/// </summary>
|
||||
public bool Contains(MessageFieldReference field)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Field == field)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a value to the result
|
||||
/// </summary>
|
||||
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
|
||||
{
|
||||
Field = field,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Search result value
|
||||
/// </summary>
|
||||
public struct SearchResultItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The field the values is for
|
||||
/// </summary>
|
||||
public MessageFieldReference Field { get; set; }
|
||||
/// <summary>
|
||||
/// The value of the field
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using CryptoExchange.Net.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
|
||||
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 SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
if (_typePropertyInfo == null)
|
||||
_typePropertyInfo = CacheTypeAttributes();
|
||||
|
||||
writer.WriteStartArray();
|
||||
var last = -1;
|
||||
foreach (var indexProps in _typePropertyInfo)
|
||||
{
|
||||
foreach (var prop in indexProps.Value)
|
||||
{
|
||||
if (prop.ArrayProperty.Index == last)
|
||||
// Don't write the same index twice
|
||||
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 = new T();
|
||||
return ParseObject(ref reader, result, 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 T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||
#else
|
||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||
#endif
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new CeDeserializationException("Not an array");
|
||||
|
||||
|
||||
if (_typePropertyInfo == null)
|
||||
_typePropertyInfo = CacheTypeAttributes();
|
||||
|
||||
int index = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var attribute in indexAttributes)
|
||||
{
|
||||
var targetType = attribute.TargetType;
|
||||
object? value = null;
|
||||
if (attribute.JsonConverter != null)
|
||||
{
|
||||
if (attribute.JsonSerializerOptions == null)
|
||||
{
|
||||
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
Converters = { attribute.JsonConverter },
|
||||
TypeInfoResolver = options.TypeInfoResolver,
|
||||
};
|
||||
}
|
||||
|
||||
var doc = JsonDocument.ParseValue(ref reader);
|
||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||
}
|
||||
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 CeDeserializationException($"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 SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||
#else
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||
#endif
|
||||
{
|
||||
var result = new SortedDictionary<int, 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;
|
||||
if (!result.TryGetValue(att.Index, out var indexList))
|
||||
{
|
||||
indexList = new List<ArrayPropertyInfo>();
|
||||
result[att.Index] = indexList;
|
||||
}
|
||||
|
||||
indexList.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 result;
|
||||
}
|
||||
|
||||
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!;
|
||||
public JsonSerializerOptions? JsonSerializerOptions { 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,95 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
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() : new BoolConverterInnerNullable();
|
||||
}
|
||||
|
||||
private class BoolConverterInnerNullable : JsonConverter<bool?>
|
||||
{
|
||||
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadBool(ref reader, typeToConvert, options);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is bool boolVal)
|
||||
writer.WriteBooleanValue(boolVal);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
|
||||
private class BoolConverterInner : JsonConverter<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadBool(ref reader, typeToConvert, options) ?? false;
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static 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))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
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}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
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,30 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for comma separated string values
|
||||
/// </summary>
|
||||
public class CommaSplitStringConverter : JsonConverter<string[]>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var str = reader.GetString();
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return [];
|
||||
|
||||
return str!.Split(',').ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
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 decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 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() : new NullableDateTimeConverterInner();
|
||||
}
|
||||
|
||||
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||
{
|
||||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadDateTime(ref reader, typeToConvert, options);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Value == default)
|
||||
writer.WriteStringValue(default(DateTime));
|
||||
else
|
||||
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
private class DateTimeConverterInner : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
var dtValue = value;
|
||||
if (dtValue == default)
|
||||
writer.WriteStringValue(default(DateTime));
|
||||
else
|
||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(DateTime))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var decValue = reader.GetDecimal();
|
||||
if (decValue == 0 || decValue < 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDecimal(decValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonTokenType.String)
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
||||
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
return reader.GetDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a double value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromDouble(double value)
|
||||
=> ParseFromDecimal((decimal)value);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a decimal value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromDecimal(decimal value)
|
||||
{
|
||||
if (value < 19999999999)
|
||||
return ConvertFromSeconds(value);
|
||||
if (value < 19999999999999)
|
||||
return ConvertFromMilliseconds(value);
|
||||
if (value < 19999999999999999)
|
||||
return ConvertFromMicroseconds(value);
|
||||
|
||||
return ConvertFromNanoseconds(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a string value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromString(string stringValue, string? resolverName)
|
||||
{
|
||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
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))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
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))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (decimalValue <= 0)
|
||||
return default;
|
||||
if (decimalValue < 19999999999)
|
||||
return ConvertFromSeconds(decimalValue);
|
||||
if (decimalValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(decimalValue);
|
||||
if (decimalValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(decimalValue);
|
||||
|
||||
return ConvertFromNanoseconds(decimalValue);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
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>
|
||||
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
[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>
|
||||
[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>
|
||||
[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>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
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();
|
||||
return ExchangeHelpers.ParseDecimal(value);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Frozen;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
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
|
||||
{
|
||||
class EnumMapping
|
||||
{
|
||||
public T Value { get; set; }
|
||||
public string StringValue { get; set; }
|
||||
|
||||
public EnumMapping(T value, string stringValue)
|
||||
{
|
||||
Value = value;
|
||||
StringValue = stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||
#else
|
||||
private static List<EnumMapping>? _mappingToEnum = null;
|
||||
private static Dictionary<T, string>? _mappingToString = null;
|
||||
#endif
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (t == null)
|
||||
{
|
||||
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
||||
{
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} 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)
|
||||
{
|
||||
isEmptyString = false;
|
||||
var enumType = typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
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 (stringValue is null)
|
||||
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))
|
||||
{
|
||||
_unknownValuesWarned.Add(stringValue!);
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.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 (_mappingToEnum != null)
|
||||
{
|
||||
EnumMapping? mapping = null;
|
||||
// Try match on full equals
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
{
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping != null)
|
||||
{
|
||||
result = mapping.Value;
|
||||
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;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
// An empty/null value will always fail when parsing, so just return here
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
result = (T)Enum.Parse(objectType, value, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateMapping()
|
||||
{
|
||||
var mappingToEnum = new List<EnumMapping>();
|
||||
var mappingToString = new Dictionary<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)
|
||||
{
|
||||
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
||||
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
||||
if (!mappingToString.ContainsKey(enumVal))
|
||||
mappingToString.Add(enumVal, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
||||
_mappingToString = mappingToString.ToFrozenDictionary();
|
||||
#else
|
||||
_mappingToEnum = mappingToEnum;
|
||||
_mappingToString = mappingToString;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <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 (_mappingToString == null)
|
||||
CreateMapping();
|
||||
|
||||
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : 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 (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
EnumMapping? mapping = null;
|
||||
// Try match on full equals
|
||||
foreach(var item in _mappingToEnum!)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
{
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping != null)
|
||||
return mapping.Value;
|
||||
|
||||
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,22 @@
|
||||
using System;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Bool converter
|
||||
/// </summary>
|
||||
public class IntBoolConverter : JsonConverter<bool>
|
||||
{
|
||||
private readonly int _trueValue;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="trueValue">The int value representing the true value</param>
|
||||
public IntBoolConverter(int trueValue)
|
||||
{
|
||||
_trueValue = trueValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.Number)
|
||||
return false;
|
||||
|
||||
return reader.GetDecimal() == _trueValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(_trueValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON REST message handler
|
||||
/// </summary>
|
||||
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||
{
|
||||
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||
|
||||
/// <summary>
|
||||
/// Empty rate limit error
|
||||
/// </summary>
|
||||
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool RequiresSeekableStream => false;
|
||||
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream)
|
||||
{
|
||||
// Handle retry after header
|
||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (retryAfterHeader.Value?.Any() != true)
|
||||
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||
|
||||
var value = retryAfterHeader.Value.First();
|
||||
if (int.TryParse(value, out var seconds))
|
||||
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
|
||||
|
||||
if (DateTime.TryParse(value, out var datetime))
|
||||
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
|
||||
|
||||
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask<Error> ParseErrorResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<Error?> CheckForErrorResponse(
|
||||
RequestDefinition request,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream) => new ValueTask<Error?>((Error?)null);
|
||||
|
||||
/// <summary>
|
||||
/// Read the response into a JsonDocument object
|
||||
/// </summary>
|
||||
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
||||
return (null, document);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
return (result, null);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return (default, new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
|
||||
}
|
||||
}
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
|
||||
/// </summary>
|
||||
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Message evaluators
|
||||
/// </summary>
|
||||
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
|
||||
|
||||
private readonly SearchResult _searchResult = new();
|
||||
|
||||
private bool _hasArraySearches;
|
||||
private bool _initialized;
|
||||
private int _maxSearchDepth;
|
||||
private MessageTypeDefinition? _topEvaluator;
|
||||
private List<MessageEvalutorFieldReference>? _searchFields;
|
||||
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
|
||||
private Dictionary<Type, Func<object, string?>>? _mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Add a mapping of a specific object of a type to a specific topic
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to get topic for</typeparam>
|
||||
/// <param name="mapping">The topic retrieve delegate</param>
|
||||
protected void AddTopicMapping<T>(Func<T, string?> mapping)
|
||||
{
|
||||
_mapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||
_mapping.Add(typeof(T), x => mapping((T)x));
|
||||
}
|
||||
|
||||
private void InitializeConverter()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_maxSearchDepth = int.MinValue;
|
||||
_searchFields = new List<MessageEvalutorFieldReference>();
|
||||
foreach (var evaluator in TypeEvaluators)
|
||||
{
|
||||
_topEvaluator ??= evaluator;
|
||||
foreach (var field in evaluator.Fields)
|
||||
{
|
||||
var overlapping = _searchFields.Where(otherField =>
|
||||
{
|
||||
if (field is PropertyFieldReference propRef
|
||||
&& otherField.Field is PropertyFieldReference otherPropRef)
|
||||
{
|
||||
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
|
||||
}
|
||||
else if (field is ArrayFieldReference arrayRef
|
||||
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
|
||||
{
|
||||
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).ToList();
|
||||
|
||||
if (overlapping.Any())
|
||||
{
|
||||
foreach (var overlap in overlapping)
|
||||
overlap.OverlappingField = true;
|
||||
}
|
||||
|
||||
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
|
||||
if (field is ArrayFieldReference arrayField)
|
||||
{
|
||||
_hasArraySearches = true;
|
||||
existingSameSearchField = _searchFields.Where(x =>
|
||||
x.Field is ArrayFieldReference arrayFieldRef
|
||||
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
|
||||
&& arrayFieldRef.Depth == arrayField.Depth
|
||||
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
|
||||
}
|
||||
else if (field is PropertyFieldReference propField)
|
||||
{
|
||||
existingSameSearchField = _searchFields.Where(x =>
|
||||
x.Field is PropertyFieldReference propFieldRef
|
||||
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
|
||||
&& propFieldRef.Depth == propField.Depth
|
||||
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
|
||||
}
|
||||
|
||||
foreach(var sameSearchField in existingSameSearchField)
|
||||
{
|
||||
if (sameSearchField.SkipReading == true
|
||||
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
|
||||
{
|
||||
sameSearchField.SkipReading = false;
|
||||
}
|
||||
|
||||
if (evaluator.ForceIfFound)
|
||||
{
|
||||
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
|
||||
throw new Exception("Invalid config");
|
||||
|
||||
//sameSearchField.ForceEvaluator = evaluator;
|
||||
}
|
||||
}
|
||||
|
||||
_searchFields.Add(new MessageEvalutorFieldReference(field)
|
||||
{
|
||||
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
|
||||
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
|
||||
OverlappingField = overlapping.Any()
|
||||
});
|
||||
|
||||
if (field.Depth > _maxSearchDepth)
|
||||
_maxSearchDepth = field.Depth;
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTopicFilter(object deserializedObject)
|
||||
{
|
||||
if (_mapping == null)
|
||||
return null;
|
||||
|
||||
// Cache the found type for future
|
||||
var currentType = deserializedObject.GetType();
|
||||
if (_baseTypeMapping != null)
|
||||
{
|
||||
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
|
||||
return typeMapping(deserializedObject);
|
||||
}
|
||||
|
||||
var mappedBase = false;
|
||||
while (currentType != null)
|
||||
{
|
||||
if (_mapping.TryGetValue(currentType, out var mapping))
|
||||
{
|
||||
if (mappedBase)
|
||||
{
|
||||
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
|
||||
}
|
||||
|
||||
return mapping(deserializedObject);
|
||||
}
|
||||
|
||||
mappedBase = true;
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
InitializeConverter();
|
||||
|
||||
int? arrayIndex = null;
|
||||
|
||||
_searchResult.Clear();
|
||||
var reader = new Utf8JsonReader(data);
|
||||
while (reader.Read())
|
||||
{
|
||||
if ((reader.TokenType == JsonTokenType.StartArray
|
||||
|| reader.TokenType == JsonTokenType.StartObject)
|
||||
&& reader.CurrentDepth == _maxSearchDepth)
|
||||
{
|
||||
// There is no field we need to search for on a depth deeper than this, skip
|
||||
reader.Skip();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
arrayIndex = -1;
|
||||
else if (reader.TokenType == JsonTokenType.EndArray)
|
||||
arrayIndex = null;
|
||||
else if (arrayIndex != null)
|
||||
arrayIndex++;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName
|
||||
|| arrayIndex != null && _hasArraySearches)
|
||||
{
|
||||
bool written = false;
|
||||
|
||||
string? value = null;
|
||||
byte[]? propName = null;
|
||||
foreach (var field in _searchFields!)
|
||||
{
|
||||
if (field.Field.Depth != reader.CurrentDepth)
|
||||
continue;
|
||||
|
||||
bool readArrayValues = false;
|
||||
if (field.Field is PropertyFieldReference propFieldRef)
|
||||
{
|
||||
if (propName == null)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
|
||||
continue;
|
||||
|
||||
propName = propFieldRef.PropertyName;
|
||||
readArrayValues = propFieldRef.ArrayValues;
|
||||
reader.Read();
|
||||
}
|
||||
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (field.Field is ArrayFieldReference arrayFieldRef)
|
||||
{
|
||||
if (propName != null)
|
||||
continue;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
if (arrayFieldRef.ArrayIndex != arrayIndex)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!field.SkipReading)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
if (readArrayValues)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
// error
|
||||
return null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
reader.Read();// Read start array
|
||||
bool first = true;
|
||||
while(reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
if (!first)
|
||||
sb.Append(",");
|
||||
|
||||
first = false;
|
||||
sb.Append(reader.GetString());
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
value = first ? null : sb.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Number:
|
||||
value = reader.GetDecimal().ToString();
|
||||
break;
|
||||
case JsonTokenType.String:
|
||||
value = reader.GetString()!;
|
||||
break;
|
||||
case JsonTokenType.True:
|
||||
case JsonTokenType.False:
|
||||
value = reader.GetBoolean().ToString()!;
|
||||
break;
|
||||
case JsonTokenType.Null:
|
||||
value = null;
|
||||
break;
|
||||
case JsonTokenType.StartObject:
|
||||
case JsonTokenType.StartArray:
|
||||
value = null;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.Field.Constraint != null
|
||||
&& !field.Field.Constraint(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_searchResult.Write(field.Field, value);
|
||||
|
||||
if (field.ForceEvaluator != null)
|
||||
{
|
||||
if (field.ForceEvaluator.StaticIdentifier != null)
|
||||
return field.ForceEvaluator.StaticIdentifier;
|
||||
|
||||
// Force the immediate return upon encountering this field
|
||||
return field.ForceEvaluator.GetMessageType(_searchResult);
|
||||
}
|
||||
|
||||
written = true;
|
||||
if (!field.OverlappingField)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!written)
|
||||
continue;
|
||||
|
||||
if (_topEvaluator!.Satisfied(_searchResult))
|
||||
return _topEvaluator.GetMessageType(_searchResult);
|
||||
|
||||
if (_searchFields.Count == _searchResult.Count)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var evaluator in TypeEvaluators)
|
||||
{
|
||||
if (evaluator.Satisfied(_searchResult))
|
||||
return evaluator.GetMessageType(_searchResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <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 virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
|
||||
/// </summary>
|
||||
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
var reader = new Utf8JsonReader(data);
|
||||
var jsonDocument = JsonDocument.ParseValue(ref reader);
|
||||
|
||||
return GetTypeIdentifier(jsonDocument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the message identifier for this document
|
||||
/// </summary>
|
||||
protected abstract string? GetTypeIdentifier(JsonDocument document);
|
||||
|
||||
/// <summary>
|
||||
/// Get optional topic filter, for example a symbol name
|
||||
/// </summary>
|
||||
public virtual string? GetTopicFilter(object deserializedObject) => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for a path, or an emtpy string if not found
|
||||
/// </summary>
|
||||
protected string StringOrEmpty(JsonDocument document, string path)
|
||||
{
|
||||
if (!document.RootElement.TryGetProperty(path, out var element))
|
||||
return string.Empty;
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
return element.GetString() ?? string.Empty;
|
||||
else if (element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetDecimal().ToString();
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
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,58 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for parsing object or array responses
|
||||
/// </summary>
|
||||
public class ObjectOrArrayConverter : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => true;
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(type)!;
|
||||
}
|
||||
|
||||
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
|
||||
{
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
|
||||
{
|
||||
// Object to object
|
||||
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
|
||||
}
|
||||
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
|
||||
{
|
||||
// Array to array
|
||||
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
|
||||
}
|
||||
else if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
// Array to object
|
||||
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
|
||||
return default;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Object to array
|
||||
JsonDocument.ParseValue(ref reader);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,21 @@
|
||||
using System;
|
||||
|
||||
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,58 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
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,44 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
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,27 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||
{
|
||||
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;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>6.1.3</PackageVersion>
|
||||
<AssemblyVersion>6.1.3</AssemblyVersion>
|
||||
<FileVersion>6.1.3</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>10.4.1</PackageVersion>
|
||||
<AssemblyVersion>10.4.1</AssemblyVersion>
|
||||
<FileVersion>10.4.1</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;CryptoExchange.Net</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>6.1.3 - Fix for concurrency exception in socket subscription</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<LangVersion>latest</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,29 +37,27 @@
|
||||
<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="10.0.101">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
||||
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Client Packages">
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception during deserialization
|
||||
/// </summary>
|
||||
public class CeDeserializationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CeDeserializationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -8,14 +16,29 @@ 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>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
private static object _idLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a value between a min and max
|
||||
@@ -55,14 +78,17 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else if(roundingType == RoundingType.Up)
|
||||
{
|
||||
if (offset != 0)
|
||||
value += (step.Value - offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
value -= offset;
|
||||
else value += (step.Value - offset);
|
||||
}
|
||||
|
||||
value = RoundDown(value, 8);
|
||||
|
||||
return value.Normalize();
|
||||
}
|
||||
@@ -86,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>
|
||||
@@ -107,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>
|
||||
@@ -128,18 +188,317 @@ namespace CryptoExchange.Net
|
||||
return value / 1.000000000000000000000000000000000m;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int NextId()
|
||||
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)
|
||||
{
|
||||
lock (_idLock)
|
||||
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 number of digits</param>
|
||||
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 long value between two values
|
||||
/// </summary>
|
||||
/// <param name="minValue">Min value</param>
|
||||
/// <param name="maxValue">Max value</param>
|
||||
/// <returns></returns>
|
||||
public static long RandomLong(long minValue, long maxValue)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
var buf = RandomNumberGenerator.GetBytes(8);
|
||||
#else
|
||||
byte[] buf = new byte[8];
|
||||
var random = new Random();
|
||||
random.NextBytes(buf);
|
||||
#endif
|
||||
long longRand = BitConverter.ToInt64(buf, 0);
|
||||
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
_lastId += 1;
|
||||
return _lastId;
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates received from a websocket subscriptions and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The queued update type</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||
Func<DataEvent<T>, Task> asyncHandler,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null)
|
||||
{
|
||||
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||
{
|
||||
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
|
||||
_ = processor.StopAsync(true);
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The queued update type</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
/// <param name="ct">Cancellation token to stop the processing</param>
|
||||
public static async Task ProcessQueuedAsync<T>(
|
||||
Func<Action<T>, Task> subscribeCall,
|
||||
Func<T, Task> asyncHandler,
|
||||
CancellationToken ct,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null)
|
||||
{
|
||||
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
ct.Register(async () =>
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
});
|
||||
|
||||
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates received from a websocket subscriptions and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="TEventType">The type of the queued item</typeparam>
|
||||
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<TEventType, TOutputType>(
|
||||
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
|
||||
Func<DataEvent<TOutputType>, Task> asyncHandler,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null
|
||||
)
|
||||
{
|
||||
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
|
||||
return asyncHandler.Invoke(mapper.Invoke(update));
|
||||
}, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
var result = await subscribeCall(processor).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||
{
|
||||
if (upd == SubscriptionStatus.Closed)
|
||||
_ = processor.StopAsync(true);
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a decimal value from a string
|
||||
/// </summary>
|
||||
public static decimal? ParseDecimal(string? value)
|
||||
{
|
||||
// Value is null or empty is the most common case to return null so check before trying to parse
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
// Try parse, only fails for these reasons:
|
||||
// 1. string is null or empty
|
||||
// 2. value is larger or smaller than decimal max/min
|
||||
// 3. unparsable format
|
||||
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
||||
return decValue;
|
||||
|
||||
// Check for values which should be parsed to null
|
||||
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Infinity value should be parsed to min/max value
|
||||
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
return decimal.MaxValue;
|
||||
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
return decimal.MinValue;
|
||||
|
||||
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
||||
{
|
||||
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
|
||||
// assume overflow
|
||||
if (overflowValue < 0)
|
||||
return decimal.MinValue;
|
||||
else
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
|
||||
// Unknown decimal format, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
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 => x.SharedSymbol));
|
||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||
return;
|
||||
|
||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the specific topic has been cached
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id</param>
|
||||
public static bool HasCached(string topicId)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
return exchangeInfo.Symbols.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a specific exchange(topic) support the provided symbol
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="symbolName">The symbol name</param>
|
||||
public static bool SupportsSymbol(string topicId, string symbolName)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a specific exchange(topic) support the provided symbol
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="symbol">The symbol info</param>
|
||||
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
return exchangeInfo.Symbols.Any(x =>
|
||||
x.Value.TradingMode == symbol.TradingMode
|
||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all symbols for a specific base asset
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="baseAsset">Base asset name</param>
|
||||
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return [];
|
||||
|
||||
return exchangeInfo.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <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,15 +1,15 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -29,18 +29,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>
|
||||
@@ -52,18 +40,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>
|
||||
@@ -76,19 +52,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>
|
||||
/// Create a query string of the specified parameters
|
||||
/// </summary>
|
||||
@@ -96,27 +59,82 @@ 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)
|
||||
var uriString = new StringBuilder();
|
||||
bool first = true;
|
||||
foreach(var parameter in parameters)
|
||||
{
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
if (!first)
|
||||
uriString.Append("&");
|
||||
|
||||
first = false;
|
||||
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
bool firstArrayValue = true;
|
||||
foreach (var entry in (object[])parameter.Value)
|
||||
{
|
||||
if (!firstArrayValue)
|
||||
uriString.Append('&');
|
||||
firstArrayValue = false;
|
||||
|
||||
uriString.Append(parameter.Key);
|
||||
uriString.Append("[]=");
|
||||
if (urlEncodeValues)
|
||||
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||
else
|
||||
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||
}
|
||||
}
|
||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||
{
|
||||
bool firstArrayValue = true;
|
||||
foreach (var entry in (object[])parameter.Value)
|
||||
{
|
||||
if (!firstArrayValue)
|
||||
uriString.Append('&');
|
||||
firstArrayValue = false;
|
||||
uriString.Append(parameter.Key);
|
||||
uriString.Append("=");
|
||||
if (urlEncodeValues)
|
||||
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||
else
|
||||
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uriString.Append('[');
|
||||
var firstArrayEntry = true;
|
||||
foreach (var entry in (object[])parameter.Value)
|
||||
{
|
||||
if (!firstArrayEntry)
|
||||
uriString.Append(',');
|
||||
|
||||
firstArrayEntry = false;
|
||||
if (urlEncodeValues)
|
||||
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||
else
|
||||
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||
}
|
||||
uriString.Append(']');
|
||||
}
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
|
||||
uriString += "&";
|
||||
uriString.Append(parameter.Key);
|
||||
uriString.Append('=');
|
||||
if (urlEncodeValues)
|
||||
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value)));
|
||||
else
|
||||
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value));
|
||||
}
|
||||
}
|
||||
|
||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
|
||||
uriString = uriString.TrimEnd('&');
|
||||
return uriString;
|
||||
return uriString.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,141 +142,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());
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static JToken? ToJToken(this string stringData, ILogger? logger = 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}";
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
return formData.ToString()!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -336,6 +240,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>
|
||||
@@ -366,120 +280,215 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Append a base url with provided path
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string AppendPath(this string url, params string[] path)
|
||||
{
|
||||
if (!url.EndsWith("/"))
|
||||
url += "/";
|
||||
|
||||
foreach (var item in path)
|
||||
url += item.Trim('/') + "/";
|
||||
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
public static string FillPathParameters(this string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
var sb = new StringBuilder(url.TrimEnd('/'));
|
||||
foreach (var subPath in path)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
sb.Append('/');
|
||||
sb.Append(subPath.Trim('/'));
|
||||
}
|
||||
return path;
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||
public static ReadOnlySpan<byte> DecompressGzip(this ReadOnlySpan<byte> data)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Port = baseUri.Port;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
using var decompressedStream = new MemoryStream();
|
||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||
deflateStream.CopyTo(decompressedStream);
|
||||
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<byte> Decompress(this ReadOnlySpan<byte> input)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using var compressStream = new MemoryStream(input.ToArray());
|
||||
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
|
||||
decompressor.CopyTo(output);
|
||||
return new ReadOnlySpan<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>
|
||||
/// Whether the account type is a futures account
|
||||
/// </summary>
|
||||
public static bool IsFuturesAccount(this SharedAccountType type) =>
|
||||
type == SharedAccountType.PerpetualLinearFutures
|
||||
|| type == SharedAccountType.DeliveryLinearFutures
|
||||
|| type == SharedAccountType.PerpetualInverseFutures
|
||||
|| type == SharedAccountType.DeliveryInverseFutures;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the account type is a margin account
|
||||
/// </summary>
|
||||
public static bool IsMarginAccount(this SharedAccountType type) =>
|
||||
type == SharedAccountType.CrossMargin
|
||||
|| type == SharedAccountType.IsolatedMargin;
|
||||
|
||||
/// <summary>
|
||||
/// Map a TradingMode value to a SharedAccountType enum value
|
||||
/// </summary>
|
||||
public static SharedAccountType ToAccountType(this TradingMode mode)
|
||||
{
|
||||
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
|
||||
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
|
||||
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
|
||||
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
|
||||
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
|
||||
|
||||
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITransferRestClient)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(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
|
||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a hex encoded string to byte array
|
||||
/// </summary>
|
||||
/// <param name="hexString"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] HexStringToBytes(this string hexString)
|
||||
{
|
||||
if (hexString.StartsWith("0x"))
|
||||
hexString = hexString.Substring(2);
|
||||
|
||||
byte[] bytes = new byte[hexString.Length / 2];
|
||||
for (int i = 0; i < hexString.Length; i += 2)
|
||||
{
|
||||
if(parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
string hexSubstring = hexString.Substring(i, 2);
|
||||
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Port = baseUri.Port;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add parameter to URI
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||
{
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
httpValueCollection.Remove(name);
|
||||
httpValueCollection.Add(name, value);
|
||||
|
||||
var ub = new UriBuilder(uri);
|
||||
ub.Query = httpValueCollection.ToString();
|
||||
|
||||
return ub.Uri;
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,47 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user