mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d77c4354a6 | |||
| 21860ddf85 | |||
| 2cffa22cc2 | |||
| 985ba9bb29 | |||
| 96f23f163d | |||
| 0e7d49991a | |||
| 3e635cf0fe | |||
| 1425c66c69 | |||
| fc3b7cc75b | |||
| 2cc2dc6ceb | |||
| 7da8cedf66 | |||
| 2cf10668dd | |||
| f1342b5ff2 | |||
| a04b636a11 | |||
| e4637ad295 | |||
| 3a1e43dabe | |||
| 10da1a7bfe | |||
| 37320ca862 | |||
| 2074a5e26f | |||
| 6b14cdbf06 |
@@ -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(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(ex.ToLogString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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("ProtoBufError: " + 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(ex.ToLogString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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("ProtobufError: " + 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</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>9.2.0</PackageVersion>
|
||||
<AssemblyVersion>9.2.0</AssemblyVersion>
|
||||
<FileVersion>9.2.0</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="9.2.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.52" />
|
||||
</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,9 @@
|
||||
#  CryptoExchange.Net.Proto
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.NetProtobuf/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.NetProtobuf) 
|
||||
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 9.2.0 - 14 Jul 2025
|
||||
* Initial release
|
||||
@@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
||||
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -80,8 +80,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
Assert.That(result.Error.Message.Contains("Invalid request"));
|
||||
Assert.That(result.Error.Message.Contains("123"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text.Json.Serialization;
|
||||
using NUnit.Framework.Legacy;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -269,9 +270,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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);
|
||||
|
||||
@@ -286,6 +296,42 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +369,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter<Test, SerializationContext>))]
|
||||
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||
record Test
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
@@ -344,9 +390,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public TestEnum? Prop7 { get; set; }
|
||||
[ArrayProperty(7)]
|
||||
public Test TestInternal { get; set; }
|
||||
[ArrayProperty(8), JsonConversion]
|
||||
public Test3 Prop8 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter<Test2, SerializationContext>))]
|
||||
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
||||
record Test2
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
|
||||
@@ -14,6 +14,7 @@ using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -24,12 +25,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = new TestClientOptions();
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||
}
|
||||
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
|
||||
{
|
||||
var errorData = accessor.Deserialize<TestError>();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using CryptoExchange.Net.Testing.Implementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -98,7 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
}
|
||||
|
||||
protected internal override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -114,12 +115,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub).Result;
|
||||
return ConnectSocketAsync(sub, default).Result;
|
||||
}
|
||||
|
||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
||||
{
|
||||
if (!message.IsJson)
|
||||
if (!message.IsValid)
|
||||
{
|
||||
return "topic";
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
|
||||
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
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -41,6 +43,10 @@ Global
|
||||
{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
|
||||
|
||||
@@ -465,10 +465,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||
{
|
||||
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 serializer.Serialize(value);
|
||||
return stringSerializer.Serialize(value);
|
||||
else
|
||||
return serializer.Serialize(parameters);
|
||||
return stringSerializer.Serialize(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Caching
|
||||
{
|
||||
internal class MemoryCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||
@@ -26,16 +28,13 @@ namespace CryptoExchange.Net.Caching
|
||||
/// <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;
|
||||
|
||||
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
||||
{
|
||||
_cache.TryRemove(key, out _);
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ namespace CryptoExchange.Net.Clients
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
@@ -68,9 +71,10 @@ namespace CryptoExchange.Net.Clients
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
ApiCredentials = apiCredentials?.Copy();
|
||||
|
||||
if (apiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,9 +90,9 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
ApiOptions.ApiCredentials = credentials;
|
||||
if (credentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
ApiCredentials = credentials?.Copy();
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -97,9 +101,9 @@ namespace CryptoExchange.Net.Clients
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
||||
if (options.ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
||||
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -66,8 +66,6 @@ namespace CryptoExchange.Net.Clients
|
||||
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(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
|
||||
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
@@ -19,6 +20,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
@@ -33,10 +35,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -241,10 +241,11 @@ namespace CryptoExchange.Net.Clients
|
||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||
if (result.Error is not CancellationRequestedError)
|
||||
{
|
||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||
if (!result)
|
||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
|
||||
else
|
||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -465,7 +466,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
// Error response
|
||||
await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||
|
||||
Error error;
|
||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||
@@ -481,7 +482,7 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
else
|
||||
{
|
||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
|
||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
|
||||
}
|
||||
|
||||
if (error.Code == null || error.Code == 0)
|
||||
@@ -490,15 +491,15 @@ namespace CryptoExchange.Net.Clients
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||
}
|
||||
|
||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||
if (typeof(T) == typeof(object))
|
||||
// Success status code and expected empty response, assume it's correct
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||
|
||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||
if (!valid)
|
||||
{
|
||||
// Invalid json
|
||||
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
@@ -525,20 +526,19 @@ namespace CryptoExchange.Net.Clients
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException));
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -603,12 +603,16 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
if (contentType == Constants.JsonContentHeader)
|
||||
{
|
||||
var serializer = CreateSerializer();
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
// Write the parameters as json in the body
|
||||
string stringData;
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
stringData = CreateSerializer().Serialize(value);
|
||||
stringData = stringSerializer.Serialize(value);
|
||||
else
|
||||
stringData = CreateSerializer().Serialize(parameters);
|
||||
stringData = stringSerializer.Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
@@ -625,11 +629,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="httpStatusCode">The response status code</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <param name="exception">Exception</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
||||
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
|
||||
{
|
||||
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
||||
return new ServerError(message);
|
||||
return new ServerError(null, "Unknown request error", exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -641,21 +645,19 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
||||
{
|
||||
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
||||
|
||||
// Handle retry after header
|
||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (retryAfterHeader.Value?.Any() != true)
|
||||
return new ServerRateLimitError(message);
|
||||
return new ServerRateLimitError();
|
||||
|
||||
var value = retryAfterHeader.Value.First();
|
||||
if (int.TryParse(value, out var seconds))
|
||||
return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||
|
||||
if (DateTime.TryParse(value, out var datetime))
|
||||
return new ServerRateLimitError(message) { RetryAfter = datetime };
|
||||
return new ServerRateLimitError() { RetryAfter = datetime };
|
||||
|
||||
return new ServerRateLimitError(message);
|
||||
return new ServerRateLimitError();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -41,6 +42,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Keep alive timeout for websocket connection
|
||||
/// </summary>
|
||||
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||
/// </summary>
|
||||
@@ -132,7 +138,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Create a message accessor instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal abstract IByteMessageAccessor CreateAccessor();
|
||||
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
|
||||
|
||||
/// <summary>
|
||||
/// Create a serializer instance
|
||||
@@ -205,9 +211,9 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException tce)
|
||||
{
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -238,7 +244,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
@@ -262,7 +268,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (subQuery != null)
|
||||
{
|
||||
// Send the request and wait for answer
|
||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
|
||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
waitEvent?.Set();
|
||||
@@ -346,7 +352,7 @@ namespace CryptoExchange.Net.Clients
|
||||
released = true;
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<THandlerResponse>(connectResult.Error!);
|
||||
}
|
||||
@@ -373,13 +379,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <param name="socket">The connection to check</param>
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return CallResult.SuccessResult;
|
||||
|
||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return connectResult;
|
||||
|
||||
@@ -560,11 +567,12 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected async virtual Task HandleConnectRateLimitedAsync()
|
||||
{
|
||||
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
|
||||
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
|
||||
{
|
||||
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
||||
_logger.AddingRetryAfterGuard(retryAfter);
|
||||
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
|
||||
RateLimiter ??= new RateLimitGate("Connection");
|
||||
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,10 +580,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Connect a socket
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket to connect</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
|
||||
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
|
||||
{
|
||||
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
|
||||
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
||||
if (connectResult)
|
||||
{
|
||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||
@@ -595,6 +604,7 @@ namespace CryptoExchange.Net.Clients
|
||||
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||
{
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
KeepAliveTimeout = KeepAliveTimeout,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||
@@ -706,7 +716,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!socketResult)
|
||||
return socketResult.AsDataless();
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult(connectResult.Error!);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Caching for JsonSerializerContext instances
|
||||
/// </summary>
|
||||
public static class JsonSerializerContextCache
|
||||
{
|
||||
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
||||
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
|
||||
{
|
||||
var contextType = typeof(T);
|
||||
if (_cache.TryGetValue(contextType, out var context))
|
||||
return context;
|
||||
|
||||
var instance = new T();
|
||||
_cache[contextType] = instance;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ using System.Text.Json;
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
@@ -16,14 +18,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// 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, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
|
||||
# else
|
||||
public class ArrayConverter<T, TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
|
||||
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||
#else
|
||||
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||
#endif
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
||||
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
|
||||
|
||||
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
@@ -39,11 +40,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
writer.WriteStartArray();
|
||||
|
||||
var valueType = typeof(T);
|
||||
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
|
||||
typeAttributes = CacheTypeAttributes(valueType);
|
||||
|
||||
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||
var last = -1;
|
||||
foreach (var prop in ordered)
|
||||
{
|
||||
@@ -72,7 +69,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
TypeInfoResolver = (TContext)Activator.CreateInstance(typeof(TContext))!,
|
||||
TypeInfoResolver = options.TypeInfoResolver,
|
||||
};
|
||||
typeOptions.Converters.Add(prop.JsonConverter);
|
||||
}
|
||||
@@ -101,77 +98,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return default;
|
||||
|
||||
var result = Activator.CreateInstance(typeof(T))!;
|
||||
return (T)ParseObject(ref reader, result, typeof(T), options);
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
|
||||
#else
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
||||
#endif
|
||||
{
|
||||
var attributes = new List<ArrayPropertyInfo>();
|
||||
var properties = type.GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||
if (att == null)
|
||||
continue;
|
||||
|
||||
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||
attributes.Add(new ArrayPropertyInfo
|
||||
{
|
||||
ArrayProperty = att,
|
||||
PropertyInfo = property,
|
||||
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
||||
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
||||
TargetType = targetType
|
||||
});
|
||||
}
|
||||
|
||||
_typeAttributesCache.TryAdd(type, attributes);
|
||||
return attributes;
|
||||
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 object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options)
|
||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||
#else
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||
#endif
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("Not an array");
|
||||
|
||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
||||
attributes = CacheTypeAttributes(objectType);
|
||||
|
||||
int index = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
|
||||
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
|
||||
if (!indexAttributes.Any())
|
||||
{
|
||||
index++;
|
||||
@@ -184,25 +133,23 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
object? value = null;
|
||||
if (attribute.JsonConverter != null)
|
||||
{
|
||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
|
||||
{
|
||||
newOptions = new JsonSerializerOptions
|
||||
if (attribute.JsonSerializerOptions == null)
|
||||
{
|
||||
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
Converters = { attribute.JsonConverter },
|
||||
TypeInfoResolver = options.TypeInfoResolver,
|
||||
};
|
||||
_converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
|
||||
}
|
||||
|
||||
var doc = JsonDocument.ParseValue(ref reader);
|
||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
|
||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
// Use default deserialization
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters((TContext)Activator.CreateInstance(typeof(TContext))!));
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -230,6 +177,50 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
||||
#else
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
||||
#endif
|
||||
{
|
||||
var attributes = new List<ArrayPropertyInfo>();
|
||||
var properties = typeof(T).GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||
if (att == null)
|
||||
continue;
|
||||
|
||||
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||
attributes.Add(new ArrayPropertyInfo
|
||||
{
|
||||
ArrayProperty = att,
|
||||
PropertyInfo = property,
|
||||
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
||||
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
||||
TargetType = targetType
|
||||
});
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private class ArrayPropertyInfo
|
||||
{
|
||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||
@@ -237,6 +228,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public JsonConverter? JsonConverter { get; set; }
|
||||
public bool DefaultDeserialization { get; set; }
|
||||
public Type TargetType { get; set; } = null!;
|
||||
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return (reader.GetString()?.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? []);
|
||||
var str = reader.GetString();
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return [];
|
||||
|
||||
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -19,22 +19,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch(OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
return ExchangeHelpers.ParseDecimal(value);
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -67,6 +67,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private static List<KeyValuePair<T, string>>? _mapping = null;
|
||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||
|
||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||
|
||||
internal class NullableEnumConverter : JsonConverter<T?>
|
||||
{
|
||||
private readonly EnumConverter<T> _enumConverter;
|
||||
@@ -77,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||
@@ -96,18 +98,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||
if (t == null)
|
||||
{
|
||||
if (isEmptyString)
|
||||
if (warn)
|
||||
{
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
if (isEmptyString)
|
||||
{
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
}
|
||||
|
||||
return new T(); // return default value
|
||||
}
|
||||
else
|
||||
@@ -116,9 +122,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
}
|
||||
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
|
||||
{
|
||||
isEmptyString = false;
|
||||
warn = false;
|
||||
var enumType = typeof(T);
|
||||
if (_mapping == null)
|
||||
_mapping = AddMapping();
|
||||
@@ -126,7 +133,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
var stringValue = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||
JsonTokenType.Number => reader.GetInt32().ToString(),
|
||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
||||
JsonTokenType.Null => null,
|
||||
@@ -145,7 +152,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
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: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||
if (!_unknownValuesWarned.Contains(stringValue))
|
||||
{
|
||||
warn = true;
|
||||
_unknownValuesWarned.Add(stringValue!);
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -184,6 +196,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_unknownValuesWarned.Contains(value))
|
||||
{
|
||||
// Check if it is an known unknown value
|
||||
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <summary>
|
||||
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver)
|
||||
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||
{
|
||||
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||
{
|
||||
@@ -33,6 +33,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
},
|
||||
TypeInfoResolver = typeResolver,
|
||||
};
|
||||
|
||||
foreach (var converter in additionalConverters)
|
||||
options.Converters.Add(converter);
|
||||
|
||||
options.TypeInfoResolver = typeResolver;
|
||||
_cache.TryAdd(typeResolver, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
|
||||
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
|
||||
|
||||
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
|
||||
{
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // Start array
|
||||
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
reader.Read();
|
||||
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
reader.Read();
|
||||
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // End array
|
||||
|
||||
var result = new T();
|
||||
result.QuantityInBaseAsset = baseQuantity;
|
||||
result.QuantityInQuoteAsset = quoteQuantity;
|
||||
result.QuantityInContracts = contractQuantity;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
if (value.QuantityInBaseAsset == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
|
||||
|
||||
if (value.QuantityInQuoteAsset == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
|
||||
|
||||
if (value.QuantityInContracts == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.QuantityInContracts.Value);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
|
||||
{
|
||||
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // Start array
|
||||
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
||||
reader.Read();
|
||||
var baseAsset = reader.GetString()!;
|
||||
reader.Read();
|
||||
var quoteAsset = reader.GetString()!;
|
||||
reader.Read();
|
||||
var timeStr = reader.GetString()!;
|
||||
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // End array
|
||||
|
||||
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
writer.WriteStringValue(value.TradingMode.ToString());
|
||||
writer.WriteStringValue(value.BaseAsset);
|
||||
writer.WriteStringValue(value.QuoteAsset);
|
||||
writer.WriteStringValue(value.DeliverTime?.ToString());
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsJson { get; set; }
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
return new CallResult<object>(GetOriginalString());
|
||||
|
||||
if (_document == null)
|
||||
@@ -61,12 +61,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
return new CallResult<object>(new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
return new CallResult<object>(new DeserializeError(info, ex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,19 +88,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
return new CallResult<T>(new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Unknown exception: {ex.Message}";
|
||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
return new CallResult<T>(new DeserializeError(info, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
@@ -117,7 +117,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var node = GetPathNode(path);
|
||||
@@ -139,7 +139,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
@@ -171,9 +171,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public List<T?>? GetValues<T>(MessagePath path)
|
||||
public T?[]? GetValues<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
@@ -183,12 +183,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||
return default;
|
||||
|
||||
return value.Value.Deserialize<List<T>>(_customSerializerOptions)!;
|
||||
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
||||
}
|
||||
|
||||
private JsonElement? GetPathNode(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
@@ -279,14 +279,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
try
|
||||
{
|
||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||
IsJson = true;
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,19 +337,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||
{
|
||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||
IsJson = false;
|
||||
IsValid = false;
|
||||
return new CallResult(new ServerError("Not a json value"));
|
||||
}
|
||||
|
||||
_document = JsonDocument.Parse(data);
|
||||
IsJson = true;
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Text.Json.Serialization.Metadata;
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
||||
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>8.8.0</PackageVersion>
|
||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
||||
<FileVersion>8.8.0</FileVersion>
|
||||
<PackageVersion>9.2.0</PackageVersion>
|
||||
<AssemblyVersion>9.2.0</AssemblyVersion>
|
||||
<FileVersion>9.2.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -27,8 +27,8 @@
|
||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="AOT" Condition=" '$(TargetFramework)' == 'NET8_0' Or '$(TargetFramework)' == 'NET9_0' ">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -37,12 +37,6 @@
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
@@ -57,10 +51,11 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Client Packages">
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -2,6 +2,7 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
@@ -112,6 +113,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>
|
||||
@@ -313,5 +342,28 @@ namespace CryptoExchange.Net
|
||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a decimal value from a string
|
||||
/// </summary>
|
||||
public static decimal? ParseDecimal(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
{
|
||||
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset, x.QuoteAsset, (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
||||
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||
return;
|
||||
|
||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset, x.QuoteAsset, (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -443,6 +443,8 @@ namespace CryptoExchange.Net
|
||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||
|
||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||
@@ -450,6 +452,10 @@ namespace CryptoExchange.Net
|
||||
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)!);
|
||||
@@ -471,6 +477,12 @@ namespace CryptoExchange.Net
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -13,9 +14,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this a json message
|
||||
/// Is this a valid message
|
||||
/// </summary>
|
||||
bool IsJson { get; }
|
||||
bool IsValid { get; }
|
||||
/// <summary>
|
||||
/// Is the original data available for retrieval
|
||||
/// </summary>
|
||||
@@ -52,19 +53,27 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
List<T?>? GetValues<T>(MessagePath path);
|
||||
T?[]? GetValues<T>(MessagePath path);
|
||||
/// <summary>
|
||||
/// Deserialize the message into this type
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
#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
|
||||
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
||||
/// <summary>
|
||||
/// Deserialize the message into this type
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
#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
|
||||
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer interface
|
||||
/// </summary>
|
||||
public interface IMessageSerializer
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to byte array
|
||||
/// </summary>
|
||||
public interface IByteMessageSerializer: IMessageSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize an object to a string
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
byte[] Serialize<T>(T message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to string
|
||||
/// </summary>
|
||||
public interface IStringMessageSerializer: IMessageSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize an object to a string
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@@ -75,15 +76,22 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Connect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> ConnectAsync();
|
||||
Task<CallResult> ConnectAsync(CancellationToken ct);
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// Send string data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="weight"></param>
|
||||
bool Send(int id, string data, int weight);
|
||||
/// <summary>
|
||||
/// Send byte data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="weight"></param>
|
||||
bool Send(int id, byte[] data, int weight);
|
||||
/// <summary>
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
|
||||
+13
@@ -8,6 +8,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
private static readonly Action<ILogger, int, Exception?> _connecting;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
|
||||
private static readonly Action<ILogger, int, Exception?> _connectingCanceled;
|
||||
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
|
||||
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
|
||||
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
|
||||
@@ -189,6 +190,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(1030, "SocketPingTimeout"),
|
||||
"[Sckt {Id}] ping frame timeout; reconnecting socket");
|
||||
|
||||
_connectingCanceled = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1031, "ConnectingCanceled"),
|
||||
"[Sckt {SocketId}] connecting canceled");
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void SocketConnecting(
|
||||
@@ -370,5 +377,11 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_socketPingTimeout(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketConnectingCanceled(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_connectingCanceled(logger, socketId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static class RestApiClientLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
|
||||
private static readonly Action<ILogger, int?, int?, long, string?, string?, Exception?> _restApiErrorReceived;
|
||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
|
||||
@@ -25,10 +25,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
|
||||
static RestApiClientLoggingExtensions()
|
||||
{
|
||||
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
||||
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
|
||||
LogLevel.Warning,
|
||||
new EventId(4000, "RestApiErrorReceived"),
|
||||
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}");
|
||||
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
|
||||
|
||||
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
||||
LogLevel.Debug,
|
||||
@@ -92,9 +92,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
|
||||
}
|
||||
|
||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
|
||||
{
|
||||
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, null);
|
||||
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, originalData, exception);
|
||||
}
|
||||
|
||||
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
|
||||
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _failedToParse;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
|
||||
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
|
||||
private static readonly Action<ILogger, int, int, string, Exception?> _processorMatched;
|
||||
@@ -37,6 +38,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
|
||||
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
|
||||
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
|
||||
|
||||
static SocketConnectionLoggingExtension()
|
||||
{
|
||||
@@ -189,6 +191,16 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Warning,
|
||||
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
||||
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}");
|
||||
|
||||
_failedToParse = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2030, "FailedToParse"),
|
||||
"[Sckt {SocketId}] failed to parse data: {Error}");
|
||||
|
||||
_sendingByteData = LoggerMessage.Define<int, int, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(2031, "SendingByteData"),
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}");
|
||||
}
|
||||
|
||||
public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
|
||||
@@ -230,6 +242,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_receivedData(logger, socketId, originalData, null);
|
||||
}
|
||||
|
||||
public static void FailedToParse(this ILogger logger, int socketId, string error)
|
||||
{
|
||||
_failedToParse(logger, socketId, error, null);
|
||||
}
|
||||
|
||||
public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData)
|
||||
{
|
||||
_failedToEvaluateMessage(logger, socketId, originalData, null);
|
||||
@@ -246,9 +264,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_receivedMessageNotRecognized(logger, socketId, id, null);
|
||||
}
|
||||
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage)
|
||||
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex)
|
||||
{
|
||||
_failedToDeserializeMessage(logger, socketId, errorMessage, null);
|
||||
_failedToDeserializeMessage(logger, socketId, errorMessage, ex);
|
||||
}
|
||||
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
|
||||
{
|
||||
@@ -321,5 +339,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
|
||||
}
|
||||
|
||||
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
||||
{
|
||||
_sendingByteData(logger, socketId, requestId, length, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
"{Api} order book {Symbol} connection lost");
|
||||
|
||||
_orderBookDisconnected = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Warning,
|
||||
LogLevel.Debug,
|
||||
new EventId(5004, "OrderBookDisconnected"),
|
||||
"{Api} order book {Symbol} disconnected");
|
||||
|
||||
|
||||
@@ -173,9 +173,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_klineTrackerStarting(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
|
||||
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception)
|
||||
{
|
||||
_klineTrackerStartFailed(logger, symbol, error, null);
|
||||
_klineTrackerStartFailed(logger, symbol, error, exception);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStarted(this ILogger logger, string symbol)
|
||||
@@ -233,9 +233,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_tradeTrackerStarting(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
|
||||
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex)
|
||||
{
|
||||
_tradeTrackerStartFailed(logger, symbol, error, null);
|
||||
_tradeTrackerStartFailed(logger, symbol, error, ex);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStarted(this ILogger logger, string symbol)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// An alias used by the exchange for an asset commonly known by another name
|
||||
/// </summary>
|
||||
public class AssetAlias
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the asset on the exchange
|
||||
/// </summary>
|
||||
public string ExchangeAssetName { get; set; }
|
||||
/// <summary>
|
||||
/// The name of the asset as it's commonly known
|
||||
/// </summary>
|
||||
public string CommonAssetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public AssetAlias(string exchangeName, string commonName)
|
||||
{
|
||||
ExchangeAssetName = exchangeName;
|
||||
CommonAssetName = commonName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange configuration for asset aliases
|
||||
/// </summary>
|
||||
public class AssetAliasConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defined aliases
|
||||
/// </summary>
|
||||
public AssetAlias[] Aliases { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Auto convert asset names when using the Shared interfaces. Defaults to true
|
||||
/// </summary>
|
||||
public bool AutoConvertEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Map the common name to an exchange name for an asset. If there is no alias the input name is returned
|
||||
/// </summary>
|
||||
public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.SingleOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName;
|
||||
|
||||
/// <summary>
|
||||
/// Map the exchange name to a common name for an asset. If there is no alias the input name is returned
|
||||
/// </summary>
|
||||
public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,18 @@ namespace CryptoExchange.Net.Objects
|
||||
return new CallResult(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the CallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data</param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||
{
|
||||
return new CallResult<K>(data, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@@ -214,6 +226,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
@@ -232,19 +249,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
/// <param name="requestHeaders"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
@@ -256,6 +265,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
RequestId = requestId;
|
||||
OriginalData = originalData;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
@@ -276,7 +286,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -440,7 +450,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
@@ -448,7 +458,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -479,6 +489,18 @@ namespace CryptoExchange.Net.Objects
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data</param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
@@ -558,7 +580,7 @@ namespace CryptoExchange.Net.Objects
|
||||
if (ResponseLength != null)
|
||||
sb.Append($", {ResponseLength} bytes");
|
||||
if (ResponseTime != null)
|
||||
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||
sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -18,21 +18,18 @@ namespace CryptoExchange.Net.Objects
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data which caused the error
|
||||
/// Underlying exception
|
||||
/// </summary>
|
||||
public object? Data { get; set; }
|
||||
public Exception? Exception { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected Error(int? code, string message, object? data)
|
||||
protected Error (int? code, string message, Exception? exception)
|
||||
{
|
||||
Code = code;
|
||||
Message = message;
|
||||
Data = data;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,7 +38,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
|
||||
return Code != null ? $"[{GetType().Name}] {Code}: {Message}" : $"[{GetType().Name}] {Message}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +55,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,10 +76,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,25 +87,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public ServerError(string message, object? data = null) : base(null, message, data) { }
|
||||
public ServerError(string message) : base(null, message, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
public ServerError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,25 +103,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public WebError(string message, object? data = null) : base(null, message, data) { }
|
||||
public WebError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected WebError(int? code, string message, object? data): base(code, message, data) { }
|
||||
public WebError(int code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -149,17 +119,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message">The error message</param>
|
||||
/// <param name="data">The data which caused the error</param>
|
||||
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
||||
public DeserializeError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
|
||||
protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -170,17 +135,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message">Error message</param>
|
||||
/// <param name="data">Error data</param>
|
||||
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
||||
public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
|
||||
protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -191,16 +151,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
|
||||
protected ArgumentError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -216,10 +172,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,10 +189,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
|
||||
protected ClientRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,16 +200,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
|
||||
public ServerRateLimitError(string? message = null, Exception? exception = null) : base(null, "Server rate limit exceeded" + (message?.Length > 0 ? " : " + message : null), exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
protected ServerRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -270,15 +216,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
||||
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
|
||||
public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -289,15 +232,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public InvalidOperationError(string message) : base(null, message, null) { }
|
||||
public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
|
||||
protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
{
|
||||
targetOptions.ApiCredentials = ApiCredentials;
|
||||
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
||||
targetOptions.Environment = Environment;
|
||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public TimeSpan? KeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for keep alive response messages
|
||||
/// </summary>
|
||||
public TimeSpan? KeepAliveTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rate limiter for the socket connection
|
||||
/// </summary>
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace CryptoExchange.Net.Objects
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}{(exception == null ? string.Empty : (", " + exception.ToLogString()))}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
// The semaphore has already been released if the task was cancelled
|
||||
release = false;
|
||||
return new CallResult(new CancellationRequestedError());
|
||||
return new CallResult(new CancellationRequestedError(tce));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -81,11 +81,11 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
// The semaphore has already been released if the task was cancelled
|
||||
release = false;
|
||||
return new CallResult(new CancellationRequestedError());
|
||||
return new CallResult(new CancellationRequestedError(tce));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Take Profit / Stop Loss side
|
||||
/// </summary>
|
||||
public enum SharedTpSlSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Take profit
|
||||
/// </summary>
|
||||
TakeProfit,
|
||||
/// <summary>
|
||||
/// Stop loss
|
||||
/// </summary>
|
||||
StopLoss
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// The order direction when order trigger parameters are reached
|
||||
/// </summary>
|
||||
public enum SharedTriggerOrderDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Enter, Buy for Spot and long futures positions, Sell for short futures positions
|
||||
/// </summary>
|
||||
Enter,
|
||||
/// <summary>
|
||||
/// Exit, Sell for Spot and long futures positions, Buy for short futures positions
|
||||
/// </summary>
|
||||
Exit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger order status
|
||||
/// </summary>
|
||||
public enum SharedTriggerOrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Order is active
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// Order has been filled
|
||||
/// </summary>
|
||||
Filled,
|
||||
/// <summary>
|
||||
/// Trigger canceled, can be user cancelation or system cancelation due to an error
|
||||
/// </summary>
|
||||
CanceledOrRejected,
|
||||
/// <summary>
|
||||
/// Trigger order has been triggered. Resulting order might be filled or not.
|
||||
/// </summary>
|
||||
Triggered
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Price direction for trigger order
|
||||
/// </summary>
|
||||
public enum SharedTriggerPriceDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger when the price goes below the specified trigger price
|
||||
/// </summary>
|
||||
PriceBelow,
|
||||
/// <summary>
|
||||
/// Trigger when the price goes above the specified trigger price
|
||||
/// </summary>
|
||||
PriceAbove
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Price direction for trigger order
|
||||
/// </summary>
|
||||
public enum SharedTriggerPriceType
|
||||
{
|
||||
/// <summary>
|
||||
/// Last traded price
|
||||
/// </summary>
|
||||
LastPrice,
|
||||
/// <summary>
|
||||
/// Mark price
|
||||
/// </summary>
|
||||
MarkPrice,
|
||||
/// <summary>
|
||||
/// Index price
|
||||
/// </summary>
|
||||
IndexPrice
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Client for managing futures orders using a client order id
|
||||
/// </summary>
|
||||
public interface IFuturesOrderClientIdClient : ISharedClient
|
||||
public interface IFuturesOrderClientIdRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures get order by client order id request options
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Take profit / Stop loss client
|
||||
/// </summary>
|
||||
public interface IFuturesTpSlRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Set take profit and/or stop loss options
|
||||
/// </summary>
|
||||
EndpointOptions<SetTpSlRequest> SetFuturesTpSlOptions { get; }
|
||||
/// <summary>
|
||||
/// Set a take profit and/or stop loss for an open position
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedId>> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel a take profit and/or stop loss options
|
||||
/// </summary>
|
||||
EndpointOptions<CancelTpSlRequest> CancelFuturesTpSlOptions { get; }
|
||||
/// <summary>
|
||||
/// Cancel an active take profit and/or stop loss for an open position
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<bool>> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for placing trigger orders
|
||||
/// </summary>
|
||||
public interface IFuturesTriggerOrderRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place spot trigger order options
|
||||
/// </summary>
|
||||
PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Place a new trigger order
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedId>> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get trigger order request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetOrderRequest> GetFuturesTriggerOrderOptions { get; }
|
||||
/// <summary>
|
||||
/// Get info on a specific trigger order
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel trigger order request options
|
||||
/// </summary>
|
||||
EndpointOptions<CancelOrderRequest> CancelFuturesTriggerOrderOptions { get; }
|
||||
/// <summary>
|
||||
/// Cancel a trigger order
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedId>> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for retrieving the current best bid/ask price
|
||||
/// </summary>
|
||||
public interface IBookTickerRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Book ticker request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetBookTickerRequest> GetBookTickerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the best ask/bid info for a symbol
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Client for managing spot orders using a client order id
|
||||
/// </summary>
|
||||
public interface ISpotOrderClientIdClient : ISharedClient
|
||||
public interface ISpotOrderClientIdRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Spot get order by client order id request options
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for placing trigger orders
|
||||
/// </summary>
|
||||
public interface ISpotTriggerOrderRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place spot trigger order options
|
||||
/// </summary>
|
||||
PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Place a new trigger order
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedId>> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get trigger order request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetOrderRequest> GetSpotTriggerOrderOptions { get; }
|
||||
/// <summary>
|
||||
/// Get info on a specific trigger order
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedSpotTriggerOrder>> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel trigger order request options
|
||||
/// </summary>
|
||||
EndpointOptions<CancelOrderRequest> CancelSpotTriggerOrderOptions { get; }
|
||||
/// <summary>
|
||||
/// Cancel a trigger order
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedId>> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,17 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public class PlaceFuturesOrderOptions : EndpointOptions<PlaceFuturesOrderRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the API supports setting take profit / stop loss with the order
|
||||
/// </summary>
|
||||
public bool SupportsTpSl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PlaceFuturesOrderOptions() : base(true)
|
||||
public PlaceFuturesOrderOptions(bool supportsTpSl) : base(true)
|
||||
{
|
||||
SupportsTpSl = supportsTpSl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,6 +35,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
SharedTimeInForce[] supportedTimeInForce,
|
||||
SharedQuantitySupport quantitySupport)
|
||||
{
|
||||
if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null))
|
||||
return new ArgumentError("Tp/Sl parameters not supported");
|
||||
|
||||
if (request.OrderType == SharedOrderType.Other)
|
||||
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
|
||||
|
||||
@@ -38,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value))
|
||||
return new ArgumentError("Order time in force not supported");
|
||||
|
||||
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity, request.QuoteQuantity);
|
||||
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
|
||||
if (quantityError != null)
|
||||
return quantityError;
|
||||
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for placing a new spot trigger order
|
||||
/// </summary>
|
||||
public class PlaceFuturesTriggerOrderOptions : EndpointOptions<PlaceFuturesTriggerOrderRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time.
|
||||
/// </summary>
|
||||
public bool HoldsFunds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PlaceFuturesTriggerOrderOptions(bool holdsFunds) : base(true)
|
||||
{
|
||||
HoldsFunds = holdsFunds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
public Error? ValidateRequest(
|
||||
string exchange,
|
||||
PlaceFuturesTriggerOrderRequest request,
|
||||
TradingMode? tradingMode,
|
||||
TradingMode[] supportedApiTypes,
|
||||
SharedOrderSide side,
|
||||
SharedQuantitySupport quantitySupport)
|
||||
{
|
||||
var quantityError = quantitySupport.Validate(side, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity);
|
||||
if (quantityError != null)
|
||||
return quantityError;
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value))
|
||||
return new ArgumentError("Order time in force not supported");
|
||||
|
||||
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity, request.QuoteQuantity);
|
||||
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
|
||||
if (quantityError != null)
|
||||
return quantityError;
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for placing a new spot trigger order
|
||||
/// </summary>
|
||||
public class PlaceSpotTriggerOrderOptions : EndpointOptions<PlaceSpotTriggerOrderRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time.
|
||||
/// </summary>
|
||||
public bool HoldsFunds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PlaceSpotTriggerOrderOptions(bool holdsFunds) : base(true)
|
||||
{
|
||||
HoldsFunds = holdsFunds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
public Error? ValidateRequest(
|
||||
string exchange,
|
||||
PlaceSpotTriggerOrderRequest request,
|
||||
TradingMode? tradingMode,
|
||||
TradingMode[] supportedApiTypes,
|
||||
SharedQuantitySupport quantitySupport)
|
||||
{
|
||||
var quantityError = quantitySupport.Validate(request.OrderSide, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity);
|
||||
if (quantityError != null)
|
||||
return quantityError;
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to cancel a take profit / stop loss
|
||||
/// </summary>
|
||||
public record CancelTpSlRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of order to cancel
|
||||
/// </summary>
|
||||
public string? OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Position mode
|
||||
/// </summary>
|
||||
public SharedPositionMode? PositionMode { get; set; }
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public SharedPositionSide? PositionSide { get; set; }
|
||||
/// <summary>
|
||||
/// Take profit / Stop loss side
|
||||
/// </summary>
|
||||
public SharedTpSlSide? TpSlSide { get; set; }
|
||||
/// <summary>
|
||||
/// Margin mode
|
||||
/// </summary>
|
||||
public SharedMarginMode? MarginMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor for canceling by order id
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol the order is on</param>
|
||||
/// <param name="orderId">Id of the order to close</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public CancelTpSlRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
OrderId = orderId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor for canceling without order id
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol the order is on</param>
|
||||
/// <param name="mode">The position mode of the account</param>
|
||||
/// <param name="positionSide">The side of the position</param>
|
||||
/// <param name="tpSlSide">The side to cancel</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public CancelTpSlRequest(SharedSymbol symbol, SharedPositionMode mode, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
PositionMode = mode;
|
||||
PositionSide = positionSide;
|
||||
TpSlSide = tpSlSide;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to retrieve best bid/ask info for a symbol
|
||||
/// </summary>
|
||||
public record GetBookTickerRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to retrieve book ticker for</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,9 @@
|
||||
/// </summary>
|
||||
public SharedTimeInForce? TimeInForce { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order in base asset.
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order in quote asset.
|
||||
/// </summary>
|
||||
public decimal? QuoteQuantity { get; set; }
|
||||
public SharedQuantity? Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
@@ -50,6 +46,15 @@
|
||||
/// </summary>
|
||||
public decimal? Leverage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Take profit price
|
||||
/// </summary>
|
||||
public decimal? TakeProfitPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Stop loss price
|
||||
/// </summary>
|
||||
public decimal? StopLossPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -58,7 +63,6 @@
|
||||
/// <param name="side">Side of the order</param>
|
||||
/// <param name="orderType">Type of the order</param>
|
||||
/// <param name="quantity">Quantity of the order</param>
|
||||
/// <param name="quoteQuantity">Quantity of the order in quote asset</param>
|
||||
/// <param name="price">Price of the order</param>
|
||||
/// <param name="timeInForce">Time in force</param>
|
||||
/// <param name="clientOrderId">Client order id</param>
|
||||
@@ -71,8 +75,7 @@
|
||||
SharedSymbol symbol,
|
||||
SharedOrderSide side,
|
||||
SharedOrderType orderType,
|
||||
decimal? quantity = null,
|
||||
decimal? quoteQuantity = null,
|
||||
SharedQuantity? quantity = null,
|
||||
decimal? price = null,
|
||||
bool? reduceOnly = null,
|
||||
decimal? leverage = null,
|
||||
@@ -85,7 +88,6 @@
|
||||
Side = side;
|
||||
OrderType = orderType;
|
||||
Quantity = quantity;
|
||||
QuoteQuantity = quoteQuantity;
|
||||
Price = price;
|
||||
MarginMode = marginMode;
|
||||
ClientOrderId = clientOrderId;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to place a new trigger order
|
||||
/// </summary>
|
||||
public record PlaceFuturesTriggerOrderRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Client order id
|
||||
/// </summary>
|
||||
public string? ClientOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// Direction of the trigger order
|
||||
/// </summary>
|
||||
public SharedTriggerOrderDirection OrderDirection { get; set; }
|
||||
/// <summary>
|
||||
/// Price trigger direction
|
||||
/// </summary>
|
||||
public SharedTriggerPriceDirection PriceDirection { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public SharedQuantity Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
public decimal? OrderPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger price
|
||||
/// </summary>
|
||||
public decimal TriggerPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Time in force
|
||||
/// </summary>
|
||||
public SharedTimeInForce? TimeInForce { get; set; }
|
||||
/// <summary>
|
||||
/// Position mode
|
||||
/// </summary>
|
||||
public SharedPositionMode? PositionMode { get; set; }
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public SharedPositionSide PositionSide { get; set; }
|
||||
/// <summary>
|
||||
/// Margin mode
|
||||
/// </summary>
|
||||
public SharedMarginMode? MarginMode { get; set; }
|
||||
/// <summary>
|
||||
/// Leverage
|
||||
/// </summary>
|
||||
public decimal? Leverage { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger price type
|
||||
/// </summary>
|
||||
public SharedTriggerPriceType? TriggerPriceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol the order is on</param>
|
||||
/// <param name="orderDirection">Direction of the order when triggered</param>
|
||||
/// <param name="priceDirection">Price direction</param>
|
||||
/// <param name="quantity">Quantity of the order</param>
|
||||
/// <param name="positionSide">Position side</param>
|
||||
/// <param name="triggerPrice">Price at which the order should activate</param>
|
||||
/// <param name="orderPrice">Limit price for the order</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public PlaceFuturesTriggerOrderRequest(SharedSymbol symbol,
|
||||
SharedTriggerPriceDirection priceDirection,
|
||||
decimal triggerPrice,
|
||||
SharedTriggerOrderDirection orderDirection,
|
||||
SharedPositionSide positionSide,
|
||||
SharedQuantity quantity,
|
||||
decimal? orderPrice = null,
|
||||
ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
PriceDirection = priceDirection;
|
||||
PositionSide = positionSide;
|
||||
Quantity = quantity;
|
||||
OrderPrice = orderPrice;
|
||||
TriggerPrice = triggerPrice;
|
||||
OrderDirection = orderDirection;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to place a new trigger order
|
||||
/// </summary>
|
||||
public record PlaceSpotTriggerOrderRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Client order id
|
||||
/// </summary>
|
||||
public string? ClientOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// Direction of the trigger order
|
||||
/// </summary>
|
||||
public SharedOrderSide OrderSide { get; set; }
|
||||
/// <summary>
|
||||
/// Price trigger direction
|
||||
/// </summary>
|
||||
public SharedTriggerPriceDirection PriceDirection { get; set; }
|
||||
/// <summary>
|
||||
/// Time in force
|
||||
/// </summary>
|
||||
public SharedTimeInForce? TimeInForce { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public SharedQuantity Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
public decimal? OrderPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger price
|
||||
/// </summary>
|
||||
public decimal TriggerPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol the order is on</param>
|
||||
/// <param name="orderSide">Order side</param>
|
||||
/// <param name="priceDirection">Price direction</param>
|
||||
/// <param name="quantity">Quantity of the order</param>
|
||||
/// <param name="triggerPrice">Price at which the order should activate</param>
|
||||
/// <param name="orderPrice">Limit price for the order</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public PlaceSpotTriggerOrderRequest(SharedSymbol symbol,
|
||||
SharedTriggerPriceDirection priceDirection,
|
||||
decimal triggerPrice,
|
||||
SharedOrderSide orderSide,
|
||||
SharedQuantity quantity,
|
||||
decimal? orderPrice = null,
|
||||
ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
PriceDirection = priceDirection;
|
||||
Quantity = quantity;
|
||||
OrderPrice = orderPrice;
|
||||
TriggerPrice = triggerPrice;
|
||||
OrderSide = orderSide;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Set a take profit and/or stop loss for an open position
|
||||
/// </summary>
|
||||
public record SetTpSlRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Position mode
|
||||
/// </summary>
|
||||
public SharedPositionMode? PositionMode { get; set; }
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public SharedPositionSide PositionSide { get; set; }
|
||||
/// <summary>
|
||||
/// Margin mode
|
||||
/// </summary>
|
||||
public SharedMarginMode? MarginMode { get; set; }
|
||||
/// <summary>
|
||||
/// Take profit / Stop loss side
|
||||
/// </summary>
|
||||
public SharedTpSlSide TpSlSide { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity to close. Only used for some API's which require a quantity in the order. Most API's will close the full position
|
||||
/// </summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger price
|
||||
/// </summary>
|
||||
public decimal TriggerPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol of the order</param>
|
||||
/// <param name="positionSide">Position side</param>
|
||||
/// <param name="tpSlSide">Take Profit / Stop Loss side</param>
|
||||
/// <param name="triggerPrice">Trigger price</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public SetTpSlRequest(SharedSymbol symbol, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, decimal triggerPrice, ExchangeParameters? exchangeParameters = null)
|
||||
: base(symbol, exchangeParameters)
|
||||
{
|
||||
PositionSide = positionSide;
|
||||
TpSlSide = tpSlSide;
|
||||
Symbol = symbol;
|
||||
TriggerPrice = triggerPrice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,19 +11,19 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Supported quantity notations for buy limit orders
|
||||
/// </summary>
|
||||
public SharedQuantityType BuyLimit { get; }
|
||||
public SharedQuantityType BuyLimit { get; set; }
|
||||
/// <summary>
|
||||
/// Supported quantity notations for sell limit orders
|
||||
/// </summary>
|
||||
public SharedQuantityType SellLimit { get; }
|
||||
public SharedQuantityType SellLimit { get; set; }
|
||||
/// <summary>
|
||||
/// Supported quantity notations for buy market orders
|
||||
/// </summary>
|
||||
public SharedQuantityType BuyMarket { get; }
|
||||
public SharedQuantityType BuyMarket { get; set; }
|
||||
/// <summary>
|
||||
/// Supported quantity notations for sell market orders
|
||||
/// </summary>
|
||||
public SharedQuantityType SellMarket { get; }
|
||||
public SharedQuantityType SellMarket { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -36,7 +36,13 @@ namespace CryptoExchange.Net.SharedApis
|
||||
SellMarket = sellMarket;
|
||||
}
|
||||
|
||||
private SharedQuantityType GetSupportedQuantityType(SharedOrderSide side, SharedOrderType orderType)
|
||||
/// <summary>
|
||||
/// Get the supported quantity type for a specific order configuration
|
||||
/// </summary>
|
||||
/// <param name="side">Side of the order</param>
|
||||
/// <param name="orderType">Type of the order</param>
|
||||
/// <returns>The supported quantity type</returns>
|
||||
public SharedQuantityType GetSupportedQuantityType(SharedOrderSide side, SharedOrderType orderType)
|
||||
{
|
||||
if (side == SharedOrderSide.Buy && (orderType == SharedOrderType.Limit || orderType == SharedOrderType.LimitMaker)) return BuyLimit;
|
||||
if (side == SharedOrderSide.Buy && orderType == SharedOrderType.Market) return BuyMarket;
|
||||
@@ -46,25 +52,45 @@ namespace CryptoExchange.Net.SharedApis
|
||||
throw new ArgumentException("Unknown side/type combination");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get whether the API supports a specific quantity type for an order configuration
|
||||
/// </summary>
|
||||
/// <param name="side">Side of the order</param>
|
||||
/// <param name="orderType">Type of the order</param>
|
||||
/// <param name="quantityType">Type of quantity</param>
|
||||
/// <returns>True if supported, false if not</returns>
|
||||
public bool IsSupported(SharedOrderSide side, SharedOrderType orderType, SharedQuantityType quantityType)
|
||||
{
|
||||
var supportedType = GetSupportedQuantityType(side, orderType);
|
||||
if (supportedType == quantityType)
|
||||
return true;
|
||||
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
/// <param name="side"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="quantity"></param>
|
||||
/// <param name="quoteQuantity"></param>
|
||||
/// <returns></returns>
|
||||
public Error? Validate(SharedOrderSide side, SharedOrderType type, decimal? quantity, decimal? quoteQuantity)
|
||||
public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity)
|
||||
{
|
||||
var supportedType = GetSupportedQuantityType(side, type);
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAsset)
|
||||
return null;
|
||||
|
||||
if ((supportedType == SharedQuantityType.BaseAsset || supportedType == SharedQuantityType.Contracts) && quoteQuantity != null)
|
||||
return new ArgumentError($"Quote quantity not supported for {side}.{type} order, specify Quantity instead");
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null)
|
||||
return new ArgumentError($"Quantity for {side}.{type} required in base or quote asset");
|
||||
|
||||
if (supportedType == SharedQuantityType.QuoteAsset && quantity != null)
|
||||
return new ArgumentError($"Quantity not supported for {side}.{type} order, specify QuoteQuantity instead");
|
||||
if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null)
|
||||
return new ArgumentError($"Quantity for {side}.{type} required in quote asset");
|
||||
|
||||
if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null)
|
||||
return new ArgumentError($"Quantity for {side}.{type} required in base asset");
|
||||
|
||||
if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null)
|
||||
return new ArgumentError($"Quantity for {side}.{type} required in contracts");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -36,21 +36,13 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public bool? ReduceOnly { get; set; }
|
||||
/// <summary>
|
||||
/// Order quantity in the base asset or number of contracts
|
||||
/// Order quantity
|
||||
/// </summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
public SharedOrderQuantity? OrderQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity filled in the base asset or number of contracts
|
||||
/// Filled quantity
|
||||
/// </summary>
|
||||
public decimal? QuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order in quote asset
|
||||
/// </summary>
|
||||
public decimal? QuoteQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity filled in the quote asset
|
||||
/// </summary>
|
||||
public decimal? QuoteQuantityFilled { get; set; }
|
||||
public SharedOrderQuantity? QuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Order price
|
||||
/// </summary>
|
||||
@@ -89,6 +81,30 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedUserTrade? LastTrade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trigger price for a trigger order
|
||||
/// </summary>
|
||||
public decimal? TriggerPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Whether or not the is order is a trigger order
|
||||
/// </summary>
|
||||
public bool? IsTriggerOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Take profit price
|
||||
/// </summary>
|
||||
public decimal? TakeProfitPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stop loss price
|
||||
/// </summary>
|
||||
public decimal? StopLossPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this order is to close an existing position. If this is the case quantities might not be specified
|
||||
/// </summary>
|
||||
public bool? IsCloseOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger order info
|
||||
/// </summary>
|
||||
public record SharedFuturesTriggerOrder : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the trigger order
|
||||
/// </summary>
|
||||
public string TriggerOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// The id of the order that was placed when this order was activated
|
||||
/// </summary>
|
||||
public string? PlacedOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// The type of the order
|
||||
/// </summary>
|
||||
public SharedOrderType OrderType { get; set; }
|
||||
/// <summary>
|
||||
/// Status of the trigger order
|
||||
/// </summary>
|
||||
public SharedTriggerOrderStatus Status { get; set; }
|
||||
/// <summary>
|
||||
/// Time in force for the order
|
||||
/// </summary>
|
||||
public SharedTimeInForce? TimeInForce { get; set; }
|
||||
/// <summary>
|
||||
/// Order quantity
|
||||
/// </summary>
|
||||
public SharedOrderQuantity? OrderQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Filled quantity
|
||||
/// </summary>
|
||||
public SharedOrderQuantity? QuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Order price
|
||||
/// </summary>
|
||||
public decimal? OrderPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Average fill price
|
||||
/// </summary>
|
||||
public decimal? AveragePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger order direction
|
||||
/// </summary>
|
||||
public SharedTriggerOrderDirection? OrderDirection { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger price
|
||||
/// </summary>
|
||||
public decimal TriggerPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Asset the fee is in
|
||||
/// </summary>
|
||||
public string? FeeAsset { get; set; }
|
||||
/// <summary>
|
||||
/// Fee paid for the order
|
||||
/// </summary>
|
||||
public decimal? Fee { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp the order was created
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
/// <summary>
|
||||
/// Last update timestamp
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
/// <summary>
|
||||
/// Position side for futures order
|
||||
/// </summary>
|
||||
public SharedPositionSide? PositionSide { get; set; }
|
||||
/// <summary>
|
||||
/// Client order id
|
||||
/// </summary>
|
||||
public string? ClientOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedFuturesTriggerOrder(
|
||||
SharedSymbol? sharedSymbol,
|
||||
string symbol,
|
||||
string triggerOrderId,
|
||||
SharedOrderType orderType,
|
||||
SharedTriggerOrderDirection? orderDirection,
|
||||
SharedTriggerOrderStatus triggerStatus,
|
||||
decimal triggerPrice,
|
||||
SharedPositionSide? positionSide,
|
||||
DateTime? createTime)
|
||||
: base(sharedSymbol, symbol)
|
||||
{
|
||||
TriggerOrderId = triggerOrderId;
|
||||
OrderType = orderType;
|
||||
OrderDirection = orderDirection;
|
||||
Status = triggerStatus;
|
||||
CreateTime = createTime;
|
||||
TriggerPrice = triggerPrice;
|
||||
PositionSide = positionSide;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Last update time
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
/// <summary>
|
||||
/// Stop loss price for the position. Not available in all API's so might be empty even though stop loss price is set
|
||||
/// </summary>
|
||||
public decimal? StopLossPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Take profit price for the position. Not available in all API's so might be empty even though stop loss price is set
|
||||
/// </summary>
|
||||
public decimal? TakeProfitPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
|
||||
@@ -27,26 +27,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Time in force for the order
|
||||
/// </summary>
|
||||
public SharedTimeInForce? TimeInForce { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Order quantity
|
||||
/// </summary>
|
||||
public SharedOrderQuantity? OrderQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Filled quantity
|
||||
/// </summary>
|
||||
public SharedOrderQuantity? QuantityFilled { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// Order quantity in base asset
|
||||
///// </summary>
|
||||
//public decimal? Quantity { get; set; }
|
||||
///// <summary>
|
||||
///// Quantity filled in base asset, note that this quantity has not yet included trading fees paid
|
||||
///// </summary>
|
||||
//public decimal? QuantityFilled { get; set; }
|
||||
///// <summary>
|
||||
///// Order quantity in quote asset
|
||||
///// </summary>
|
||||
//public decimal? QuoteQuantity { get; set; }
|
||||
///// <summary>
|
||||
///// Quantity filled in the quote asset, note that this quantity has not yet included trading fees paid
|
||||
///// </summary>
|
||||
//public decimal? QuoteQuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Order price
|
||||
/// </summary>
|
||||
@@ -80,6 +68,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedUserTrade? LastTrade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trigger price for a trigger order
|
||||
/// </summary>
|
||||
public decimal? TriggerPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Whether or not the is order is a trigger order
|
||||
/// </summary>
|
||||
public bool IsTriggerOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
/// </summary>
|
||||
public decimal Volume { get; set; }
|
||||
/// <summary>
|
||||
/// Trade volume in quote asset in the last 24h
|
||||
/// </summary>
|
||||
public decimal? QuoteVolume { get; set; }
|
||||
/// <summary>
|
||||
/// Change percentage in the last 24h
|
||||
/// </summary>
|
||||
public decimal? ChangePercentage { get; set; }
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger order info
|
||||
/// </summary>
|
||||
public record SharedSpotTriggerOrder : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the trigger order
|
||||
/// </summary>
|
||||
public string TriggerOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// The id of the order that was placed when this order was activated
|
||||
/// </summary>
|
||||
public string? PlacedOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// The type of the order
|
||||
/// </summary>
|
||||
public SharedOrderType OrderType { get; set; }
|
||||
/// <summary>
|
||||
/// Status of the trigger order
|
||||
/// </summary>
|
||||
public SharedTriggerOrderStatus Status { get; set; }
|
||||
/// <summary>
|
||||
/// Time in force for the order
|
||||
/// </summary>
|
||||
public SharedTimeInForce? TimeInForce { get; set; }
|
||||
/// <summary>
|
||||
/// Order quantity
|
||||
/// </summary>
|
||||
public SharedOrderQuantity? OrderQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Filled quantity
|
||||
/// </summary>
|
||||
public SharedOrderQuantity? QuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Order price
|
||||
/// </summary>
|
||||
public decimal? OrderPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Average fill price
|
||||
/// </summary>
|
||||
public decimal? AveragePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger order direction
|
||||
/// </summary>
|
||||
public SharedTriggerOrderDirection OrderDirection { get; set; }
|
||||
/// <summary>
|
||||
/// Trigger price
|
||||
/// </summary>
|
||||
public decimal TriggerPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Asset the fee is in
|
||||
/// </summary>
|
||||
public string? FeeAsset { get; set; }
|
||||
/// <summary>
|
||||
/// Fee paid for the order
|
||||
/// </summary>
|
||||
public decimal? Fee { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp the order was created
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
/// <summary>
|
||||
/// Last update timestamp
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
/// <summary>
|
||||
/// Client order id
|
||||
/// </summary>
|
||||
public string? ClientOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedSpotTriggerOrder(
|
||||
SharedSymbol? sharedSymbol,
|
||||
string symbol,
|
||||
string triggerOrderId,
|
||||
SharedOrderType orderType,
|
||||
SharedTriggerOrderDirection orderDirection,
|
||||
SharedTriggerOrderStatus triggerStatus,
|
||||
decimal triggerPrice,
|
||||
DateTime? createTime)
|
||||
: base(sharedSymbol, symbol)
|
||||
{
|
||||
TriggerOrderId = triggerOrderId;
|
||||
OrderType = orderType;
|
||||
OrderDirection = orderDirection;
|
||||
Status = triggerStatus;
|
||||
CreateTime = createTime;
|
||||
TriggerPrice = triggerPrice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Quantity reference
|
||||
/// </summary>
|
||||
public record SharedQuantityReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Quantity denoted in the base asset of the symbol
|
||||
/// </summary>
|
||||
public decimal? QuantityInBaseAsset { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity denoted in the quote asset of the symbol
|
||||
/// </summary>
|
||||
public decimal? QuantityInQuoteAsset { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity denoted in the number of contracts
|
||||
/// </summary>
|
||||
public decimal? QuantityInContracts { get; set; }
|
||||
|
||||
protected SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
internal SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
|
||||
{
|
||||
QuantityInBaseAsset = baseAssetQuantity;
|
||||
QuantityInQuoteAsset = quoteAssetQuantity;
|
||||
@@ -18,6 +35,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quantity for an order
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SharedQuantityConverter))]
|
||||
public record SharedQuantity : SharedQuantityReference
|
||||
{
|
||||
private SharedQuantity(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
|
||||
@@ -25,21 +46,78 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedQuantity() : base(null, null, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Specify quantity in base asset
|
||||
/// </summary>
|
||||
public static SharedQuantity Base(decimal quantity) => new SharedQuantity(quantity, null, null);
|
||||
/// <summary>
|
||||
/// Specify quantity in quote asset
|
||||
/// </summary>
|
||||
public static SharedQuantity Quote(decimal quantity) => new SharedQuantity(null, quantity, null);
|
||||
/// <summary>
|
||||
/// Specify quantity in number of contracts
|
||||
/// </summary>
|
||||
public static SharedQuantity Contracts(decimal quantity) => new SharedQuantity(null, null, quantity);
|
||||
|
||||
public static SharedQuantity BaseFromQuote(decimal quoteQuantity, decimal price) => new SharedQuantity(Math.Round(quoteQuantity / price, 8), null, null);
|
||||
public static SharedQuantity QuoteFromBase(decimal baseQuantity, decimal price) => new SharedQuantity(Math.Round(baseQuantity * price, 8), null, null);
|
||||
public static SharedQuantity ContractsFromBase(decimal baseQuantity, decimal contractSize) => new SharedQuantity(Math.Round(baseQuantity / contractSize, 8), null, null);
|
||||
public static SharedQuantity ContractsFromQuote(decimal quoteQuantity, decimal contractSize, decimal price) => new SharedQuantity(Math.Round(quoteQuantity / price / contractSize, 8), null, null);
|
||||
/// <summary>
|
||||
/// Get the base asset quantity from a quote quantity using a price
|
||||
/// </summary>
|
||||
/// <param name="quoteQuantity">Quantity in quote asset to convert</param>
|
||||
/// <param name="price">Price to use for conversion</param>
|
||||
/// <param name="decimalPlaces">The max number of decimal places for the result</param>
|
||||
/// <param name="lotSize">The lot size (step per quantity) for the base asset</param>
|
||||
public static SharedQuantity BaseFromQuote(decimal quoteQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m)
|
||||
=> new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price, decimalPlaces, lotSize), null, null);
|
||||
/// <summary>
|
||||
/// Get the quote asset quantity from a base quantity using a price
|
||||
/// </summary>
|
||||
/// <param name="baseQuantity">Quantity in base asset to convert</param>
|
||||
/// <param name="price">Price to use for conversion</param>
|
||||
/// <param name="decimalPlaces">The max number of decimal places for the result</param>
|
||||
/// <param name="lotSize">The lot size (step per quantity) for the quote asset</param>
|
||||
public static SharedQuantity QuoteFromBase(decimal baseQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m)
|
||||
=> new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity * price, decimalPlaces, lotSize), null, null);
|
||||
/// <summary>
|
||||
/// Get a quantity in number of contracts from a base asset
|
||||
/// </summary>
|
||||
/// <param name="baseQuantity">Quantity in base asset to convert</param>
|
||||
/// <param name="contractSize">The contract size of a single contract</param>
|
||||
/// <param name="decimalPlaces">The max number of decimal places for the result</param>
|
||||
/// <param name="lotSize">The lot size (step per quantity) for the contract</param>
|
||||
public static SharedQuantity ContractsFromBase(decimal baseQuantity, decimal contractSize, int decimalPlaces = 8, decimal lotSize = 0.00000001m)
|
||||
=> new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity / contractSize, decimalPlaces, lotSize), null, null);
|
||||
/// <summary>
|
||||
/// Get a quantity in number of contracts from a quote asset
|
||||
/// </summary>
|
||||
/// <param name="quoteQuantity">Quantity in quote asset to convert</param>
|
||||
/// <param name="contractSize">The contract size of a single contract</param>
|
||||
/// <param name="price">The price to use for conversion</param>
|
||||
/// <param name="decimalPlaces">The max number of decimal places for the result</param>
|
||||
/// <param name="lotSize">The lot size (step per quantity) for the contract</param>
|
||||
public static SharedQuantity ContractsFromQuote(decimal quoteQuantity, decimal contractSize, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m)
|
||||
=> new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price / contractSize, decimalPlaces, lotSize), null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order quantity
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SharedOrderQuantityConverter))]
|
||||
public record SharedOrderQuantity : SharedQuantityReference
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedOrderQuantity(): base(null, null,null) { }
|
||||
|
||||
public SharedOrderQuantity(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedOrderQuantity(decimal? baseAssetQuantity = null, decimal? quoteAssetQuantity = null, decimal? contractQuantity = null)
|
||||
: base(baseAssetQuantity, quoteAssetQuantity, contractQuantity)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// A symbol representation based on a base and quote asset
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SharedSymbolConverter))]
|
||||
public record SharedSymbol
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -166,9 +166,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<CallResult> ConnectAsync()
|
||||
public virtual async Task<CallResult> ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
var connectResult = await ConnectInternalAsync().ConfigureAwait(false);
|
||||
var connectResult = await ConnectInternalAsync(ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return connectResult;
|
||||
|
||||
@@ -203,7 +203,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
socket.Options.CollectHttpResponseDetails = true;
|
||||
#endif
|
||||
#if NET9_0_OR_GREATER
|
||||
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
|
||||
socket.Options.KeepAliveTimeout = Parameters.KeepAliveTimeout ?? TimeSpan.FromSeconds(10);
|
||||
#endif
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
@@ -215,7 +215,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
return socket;
|
||||
}
|
||||
|
||||
private async Task<CallResult> ConnectInternalAsync()
|
||||
private async Task<CallResult> ConnectInternalAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.SocketConnecting(Id);
|
||||
try
|
||||
@@ -229,12 +229,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token);
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token, ct);
|
||||
await _socket.ConnectAsync(Uri, linked.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!_ctsSource.IsCancellationRequested)
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
_logger.SocketConnectingCanceled(Id);
|
||||
}
|
||||
else if (!_ctsSource.IsCancellationRequested)
|
||||
{
|
||||
// if _ctsSource was canceled this was already logged
|
||||
_logger.SocketConnectionFailed(Id, e.Message, e);
|
||||
@@ -246,7 +250,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
return new CallResult(new ServerRateLimitError(we.Message));
|
||||
return new CallResult(new ServerRateLimitError(we.Message, we));
|
||||
}
|
||||
#else
|
||||
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
|
||||
@@ -254,12 +258,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (we.Message.Contains("429"))
|
||||
{
|
||||
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
return new CallResult(new ServerRateLimitError(we.Message));
|
||||
return new CallResult(new ServerRateLimitError(we.Message, we));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return new CallResult(new CantConnectError());
|
||||
return new CallResult(new CantConnectError(e));
|
||||
}
|
||||
|
||||
_logger.SocketConnected(Id, Uri);
|
||||
@@ -325,7 +329,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||
|
||||
_reconnectAttempt++;
|
||||
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||
var connected = await ConnectInternalAsync(default).ConfigureAwait(false);
|
||||
if (!connected)
|
||||
{
|
||||
// Delay between reconnect attempts
|
||||
@@ -373,7 +377,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Text, Weight = weight, Bytes = bytes });
|
||||
_sendEvent.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool Send(int id, byte[] data, int weight)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing)
|
||||
return false;
|
||||
|
||||
_logger.SocketAddingBytesToSendBuffer(Id, id, data);
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Binary, Weight = weight, Bytes = data });
|
||||
_sendEvent.Set();
|
||||
return true;
|
||||
}
|
||||
@@ -528,7 +544,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), data.Type, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_logger.SocketSentBytes(Id, data.Id, data.Bytes.Length);
|
||||
}
|
||||
@@ -854,6 +870,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public DateTime SendTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message type
|
||||
/// </summary>
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The bytes to send
|
||||
/// </summary>
|
||||
|
||||
@@ -79,6 +79,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public int Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the query should wait for a response or not
|
||||
/// </summary>
|
||||
public bool ExpectsResponse { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Get the type the message should be deserialized to
|
||||
/// </summary>
|
||||
@@ -116,10 +121,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public void IsSend(TimeSpan timeout)
|
||||
{
|
||||
// Start timeout countdown
|
||||
RequestTimestamp = DateTime.UtcNow;
|
||||
_cts = new CancellationTokenSource(timeout);
|
||||
_cts.Token.Register(Timeout, false);
|
||||
if (ExpectsResponse)
|
||||
{
|
||||
// Start timeout countdown
|
||||
_cts = new CancellationTokenSource(timeout);
|
||||
_cts.Token.Register(Timeout, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Completed = true;
|
||||
Result = CallResult.SuccessResult;
|
||||
_event.Set();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -211,7 +211,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
private SocketStatus _status;
|
||||
|
||||
private readonly IMessageSerializer _serializer;
|
||||
private readonly IByteMessageAccessor _accessor;
|
||||
private IByteMessageAccessor? _stringMessageAccessor;
|
||||
private IByteMessageAccessor? _byteMessageAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary.
|
||||
@@ -258,7 +259,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
_listeners = new List<IMessageProcessor>();
|
||||
|
||||
_serializer = apiClient.CreateSerializer();
|
||||
_accessor = apiClient.CreateAccessor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -459,25 +459,37 @@ namespace CryptoExchange.Net.Sockets
|
||||
data = ApiClient.PreprocessStreamMessage(this, type, data);
|
||||
|
||||
// 2. Read data into accessor
|
||||
_accessor.Read(data);
|
||||
IByteMessageAccessor accessor;
|
||||
if (type == WebSocketMessageType.Binary)
|
||||
accessor = _stringMessageAccessor ??= ApiClient.CreateAccessor(type);
|
||||
else
|
||||
accessor = _byteMessageAccessor ??= ApiClient.CreateAccessor(type);
|
||||
|
||||
var result = accessor.Read(data);
|
||||
try
|
||||
{
|
||||
bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData;
|
||||
if (outputOriginalData)
|
||||
{
|
||||
originalData = _accessor.GetOriginalString();
|
||||
originalData = accessor.GetOriginalString();
|
||||
_logger.ReceivedData(SocketId, originalData);
|
||||
}
|
||||
|
||||
if (!accessor.IsValid)
|
||||
{
|
||||
_logger.FailedToParse(SocketId, result.Error!.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Determine the identifying properties of this message
|
||||
var listenId = ApiClient.GetListenerIdentifier(_accessor);
|
||||
var listenId = ApiClient.GetListenerIdentifier(accessor);
|
||||
if (listenId == null)
|
||||
{
|
||||
originalData = outputOriginalData ? _accessor.GetOriginalString() : "[OutputOriginalData is false]";
|
||||
originalData = outputOriginalData ? accessor.GetOriginalString() : "[OutputOriginalData is false]";
|
||||
if (!ApiClient.UnhandledMessageExpected)
|
||||
_logger.FailedToEvaluateMessage(SocketId, originalData);
|
||||
|
||||
UnhandledMessage?.Invoke(_accessor);
|
||||
UnhandledMessage?.Invoke(accessor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -494,7 +506,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_listenersLock)
|
||||
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
|
||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
||||
UnhandledMessage?.Invoke(_accessor);
|
||||
UnhandledMessage?.Invoke(accessor);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -512,7 +524,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
foreach (var processor in processors)
|
||||
{
|
||||
// 5. Determine the type to deserialize to for this processor
|
||||
var messageType = processor.GetMessageType(_accessor);
|
||||
var messageType = processor.GetMessageType(accessor);
|
||||
if (messageType == null)
|
||||
{
|
||||
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
|
||||
@@ -532,10 +544,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (deserialized == null)
|
||||
{
|
||||
var desResult = processor.Deserialize(_accessor, messageType);
|
||||
var desResult = processor.Deserialize(accessor, messageType);
|
||||
if (!desResult)
|
||||
{
|
||||
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString());
|
||||
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
|
||||
continue;
|
||||
}
|
||||
deserialized = desResult.Data;
|
||||
@@ -553,7 +565,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.UserMessageProcessingFailed(SocketId, ex.ToLogString(), ex);
|
||||
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
|
||||
if (processor is Subscription subscription)
|
||||
subscription.InvokeExceptionHandler(ex);
|
||||
}
|
||||
@@ -563,7 +575,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
finally
|
||||
{
|
||||
_accessor.Clear();
|
||||
accessor.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,7 +583,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Connect the websocket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<CallResult> ConnectAsync() => await _socket.ConnectAsync().ConfigureAwait(false);
|
||||
public async Task<CallResult> ConnectAsync(CancellationToken ct) => await _socket.ConnectAsync(ct).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the underlying socket
|
||||
@@ -825,8 +837,55 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="weight">The weight of the message</param>
|
||||
public virtual CallResult Send<T>(int requestId, T obj, int weight)
|
||||
{
|
||||
var data = obj is string str ? str : _serializer.Serialize(obj!);
|
||||
return Send(requestId, data, weight);
|
||||
if (_serializer is IByteMessageSerializer byteSerializer)
|
||||
{
|
||||
return SendBytes(requestId, byteSerializer.Serialize(obj), weight);
|
||||
}
|
||||
else if (_serializer is IStringMessageSerializer stringSerializer)
|
||||
{
|
||||
if (obj is string str)
|
||||
return Send(requestId, str, weight);
|
||||
|
||||
str = stringSerializer.Serialize(obj);
|
||||
return Send(requestId, str, weight);
|
||||
}
|
||||
|
||||
throw new Exception("Unknown serializer when sending message");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send byte data over the websocket connection
|
||||
/// </summary>
|
||||
/// <param name="data">The data to send</param>
|
||||
/// <param name="weight">The weight of the message</param>
|
||||
/// <param name="requestId">The id of the request</param>
|
||||
public virtual CallResult SendBytes(int requestId, byte[] data, int weight)
|
||||
{
|
||||
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
|
||||
{
|
||||
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
|
||||
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
|
||||
return new CallResult(new InvalidOperationError(info));
|
||||
}
|
||||
|
||||
if (!_socket.IsOpen)
|
||||
{
|
||||
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId);
|
||||
return new CallResult(new WebError("Failed to send message, socket no longer open"));
|
||||
}
|
||||
|
||||
_logger.SendingByteData(SocketId, requestId, data.Length);
|
||||
try
|
||||
{
|
||||
if (!_socket.Send(requestId, data, weight))
|
||||
return new CallResult(new WebError("Failed to send message, connection not open"));
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -860,7 +919,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new CallResult(new WebError("Failed to send message: " + ex.Message));
|
||||
return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,7 +1041,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
internal async Task<CallResult> ResubscribeAsync(Subscription subscription)
|
||||
{
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new UnknownError("Socket is not connected"));
|
||||
return new CallResult(new WebError("Socket is not connected"));
|
||||
|
||||
var subQuery = subscription.GetSubQuery(this);
|
||||
if (subQuery == null)
|
||||
@@ -1036,7 +1095,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.PeriodicSendFailed(SocketId, identifier, ex.ToLogString(), ex);
|
||||
_logger.PeriodicSendFailed(SocketId, identifier, ex.Message, ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,19 +10,23 @@ using System.Text.Json.Serialization;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
|
||||
#pragma warning disable IL2026
|
||||
#pragma warning disable IL2070
|
||||
#pragma warning disable IL2075
|
||||
#pragma warning disable IL3050
|
||||
|
||||
namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
internal class SystemTextJsonComparer
|
||||
{
|
||||
internal static void CompareData(
|
||||
string method,
|
||||
object resultData,
|
||||
object? resultData,
|
||||
string json,
|
||||
string? nestedJsonProperty,
|
||||
List<string>? ignoreProperties = null,
|
||||
bool userSingleArrayItem = false)
|
||||
{
|
||||
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault()));
|
||||
var jsonObject = JsonDocument.Parse(json).RootElement;
|
||||
if (nestedJsonProperty != null)
|
||||
{
|
||||
@@ -39,6 +43,18 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
if (userSingleArrayItem)
|
||||
jsonObject = jsonObject[0];
|
||||
|
||||
|
||||
if (resultData == null)
|
||||
{
|
||||
if (jsonObject.ValueKind == JsonValueKind.Null)
|
||||
return;
|
||||
|
||||
if (jsonObject.ValueKind == JsonValueKind.Object && jsonObject.GetPropertyCount() == 0)
|
||||
return;
|
||||
|
||||
throw new Exception("ResultData null");
|
||||
}
|
||||
|
||||
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||
{
|
||||
var dict = (IDictionary)resultData;
|
||||
@@ -97,7 +113,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
if (jsonConverter != typeof(ArrayConverter<,>))
|
||||
if (jsonConverter != typeof(ArrayConverter<>))
|
||||
// Not array converter?
|
||||
continue;
|
||||
|
||||
@@ -124,7 +140,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
int i = 0;
|
||||
foreach (var item in jsonObject.EnumerateArray())
|
||||
{
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
@@ -237,7 +253,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
throw new Exception("Enumeration not moved; incorrect amount of results?");
|
||||
|
||||
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter<,>))
|
||||
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter<>))
|
||||
// Custom converter for the type, skip
|
||||
continue;
|
||||
|
||||
@@ -257,7 +273,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
if (jsonConverter != typeof(ArrayConverter<,>))
|
||||
if (jsonConverter != typeof(ArrayConverter<>))
|
||||
// Not array converter?
|
||||
continue;
|
||||
|
||||
@@ -322,7 +338,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
if (jsonConverter != typeof(ArrayConverter<,>))
|
||||
if (jsonConverter != typeof(ArrayConverter<>))
|
||||
// Not array converter?
|
||||
continue;
|
||||
|
||||
@@ -411,6 +427,11 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
if (dec != value)
|
||||
throw new Exception($"{method}: {property} not equal: {dec} vs {value}");
|
||||
}
|
||||
else if (objectValue is double dbl)
|
||||
{
|
||||
if ((decimal)dbl != value)
|
||||
throw new Exception($"{method}: {property} not equal: {dbl} vs {value}");
|
||||
}
|
||||
else if(objectValue is string objStr)
|
||||
{
|
||||
if (objStr != value.ToString())
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
#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.Testing.Implementations
|
||||
{
|
||||
internal class TestSocket : IWebsocket
|
||||
@@ -47,7 +51,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
public Task<CallResult> ConnectAsync()
|
||||
public Task<CallResult> ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
Connected = CanConnect;
|
||||
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
@@ -63,6 +67,17 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Send(int requestId, byte[] data, int weight)
|
||||
{
|
||||
if (!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
OnMessageSend?.Invoke(Encoding.UTF8.GetString(data));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
Connected = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
@@ -96,5 +97,35 @@ namespace CryptoExchange.Net.Testing
|
||||
|
||||
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start an order book implementation and expect it to sync and produce an update
|
||||
/// </summary>
|
||||
public async Task TestOrderBook(ISymbolOrderBook book)
|
||||
{
|
||||
if (!ShouldRun())
|
||||
return;
|
||||
|
||||
var bookHasChanged = false;
|
||||
book.OnStatusChange += (_, news) =>
|
||||
{
|
||||
if (news == OrderBookStatus.Reconnecting)
|
||||
throw new Exception($"Book reconnecting");
|
||||
};
|
||||
book.OnOrderBookUpdate += (change) =>
|
||||
{
|
||||
bookHasChanged = true;
|
||||
};
|
||||
|
||||
var result = await book.StartAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
throw new Exception($"Book failed to start: " + result.Error);
|
||||
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
await book.StopAsync().ConfigureAwait(false);
|
||||
|
||||
if (!bookHasChanged)
|
||||
throw new Exception($"Expected book to have changed at least once");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace CryptoExchange.Net.Testing
|
||||
string? nestedJsonProperty = null,
|
||||
List<string>? ignoreProperties = null,
|
||||
bool useSingleArrayItem = false,
|
||||
bool skipResponseValidation = false)
|
||||
bool skipResponseValidation = false)
|
||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for executing websocket API integration tests
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient">Client type</typeparam>
|
||||
public abstract class SocketIntegrationTest<TClient>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a client instance
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory"></param>
|
||||
/// <returns></returns>
|
||||
public abstract TClient GetClient(ILoggerFactory loggerFactory);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
||||
/// </summary>
|
||||
public virtual bool Run { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation.
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected TClient CreateClient()
|
||||
{
|
||||
var fact = new LoggerFactory();
|
||||
fact.AddProvider(new TraceLoggerProvider());
|
||||
return GetClient(fact);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if integration tests should be executed
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected bool ShouldRun()
|
||||
{
|
||||
var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION");
|
||||
if (!Run && integrationTests != "1")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a REST endpoint call and check for any errors or warnings.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the update</typeparam>
|
||||
/// <param name="expression">The call expression</param>
|
||||
/// <param name="expectUpdate">Whether an update is expected</param>
|
||||
/// <param name="authRequest">Whether this is an authenticated request</param>
|
||||
public async Task RunAndCheckUpdate<T>(Expression<Func<TClient, Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>>> expression, bool expectUpdate, bool authRequest)
|
||||
{
|
||||
if (!ShouldRun())
|
||||
return;
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
var expressionBody = (MethodCallExpression)expression.Body;
|
||||
if (authRequest && !Authenticated)
|
||||
{
|
||||
Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
|
||||
var evnt = new ManualResetEvent(!expectUpdate);
|
||||
DataEvent<T>? receivedUpdate = null;
|
||||
var updateHandler = (DataEvent<T> update) =>
|
||||
{
|
||||
receivedUpdate = update;
|
||||
evnt.Set();
|
||||
};
|
||||
|
||||
CallResult<UpdateSubscription> result;
|
||||
try
|
||||
{
|
||||
result = await expression.Compile().Invoke(client, updateHandler).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
|
||||
if (!result.Success)
|
||||
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
||||
|
||||
evnt.WaitOne(TimeSpan.FromSeconds(10));
|
||||
|
||||
if (expectUpdate && receivedUpdate == null)
|
||||
throw new Exception($"Method {expressionBody.Method.Name} has not received an update");
|
||||
|
||||
await result.Data.CloseAsync().ConfigureAwait(false);
|
||||
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Validator for websocket subscriptions, checking expected requests and responses and comparing update models
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient"></typeparam>
|
||||
public class SocketRequestValidator<TClient> where TClient : BaseSocketClient
|
||||
{
|
||||
private readonly string _baseAddress = "wss://localhost";
|
||||
private readonly string _folder;
|
||||
private readonly string? _nestedPropertyForCompare;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder for json test values</param>
|
||||
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||
public SocketRequestValidator(string folder, string? nestedPropertyForCompare = null)
|
||||
{
|
||||
_folder = folder;
|
||||
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a subscription
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||
/// <param name="client">Client to test</param>
|
||||
/// <param name="methodInvoke">Subscription method invocation</param>
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="responseMapper">Chose nested property to use for comparing</param>
|
||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||
/// <param name="useSingleArrayItem">Use the first item of an array update</param>
|
||||
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task ValidateAsync<TResponse>(
|
||||
TClient client,
|
||||
Func<TClient, Task<CallResult<TResponse>>> methodInvoke,
|
||||
string name,
|
||||
Func<TResponse, object>? responseMapper = null,
|
||||
string? nestedJsonProperty = null,
|
||||
List<string>? ignoreProperties = null,
|
||||
bool useSingleArrayItem = false,
|
||||
bool skipResponseValidation = false)
|
||||
{
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
|
||||
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||
FileStream file ;
|
||||
try
|
||||
{
|
||||
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new Exception("Response file not found");
|
||||
}
|
||||
|
||||
var buffer = new byte[file.Length];
|
||||
await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false);
|
||||
file.Close();
|
||||
|
||||
var data = Encoding.UTF8.GetString(buffer);
|
||||
using var reader = new StringReader(data);
|
||||
|
||||
var socket = TestHelpers.ConfigureSocketClient(client, _baseAddress);
|
||||
|
||||
var waiter = new AutoResetEvent(false);
|
||||
string? lastMessage = null;
|
||||
socket.OnMessageSend += (x) =>
|
||||
{
|
||||
lastMessage = x;
|
||||
waiter.Set();
|
||||
};
|
||||
|
||||
// Invoke subscription method
|
||||
var task = methodInvoke(client);
|
||||
|
||||
var replaceValues = new Dictionary<string, string>();
|
||||
while (true)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if (line.StartsWith("> "))
|
||||
{
|
||||
// Expect a message from client to server
|
||||
waiter.WaitOne(TimeSpan.FromSeconds(5));
|
||||
|
||||
if (lastMessage == null)
|
||||
throw new Exception($"{name} expected {line} to be send to server but did not receive anything");
|
||||
|
||||
var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement;
|
||||
var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement;
|
||||
if (expectedJson.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var item in expectedJson.EnumerateObject())
|
||||
{
|
||||
if (item.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var innerItem in item.Value.EnumerateObject())
|
||||
{
|
||||
if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|"))
|
||||
{
|
||||
// |x| values are used to replace parts of response messages
|
||||
if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop))
|
||||
continue;
|
||||
|
||||
replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|"))
|
||||
{
|
||||
// |x| values are used to replace parts of response messages
|
||||
if (!lastMessageJson.TryGetProperty(item.Name, out var prop))
|
||||
continue;
|
||||
|
||||
replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!);
|
||||
}
|
||||
else if (!lastMessageJson.TryGetProperty(item.Name, out var prop))
|
||||
{
|
||||
}
|
||||
else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true)
|
||||
{
|
||||
throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO check arrays and sub-objects
|
||||
|
||||
}
|
||||
}
|
||||
// TODO check arrays and sub-objects
|
||||
|
||||
}
|
||||
}
|
||||
else if (line.StartsWith("< "))
|
||||
{
|
||||
// Expect a message from server to client
|
||||
foreach(var item in replaceValues)
|
||||
line = line.Replace(item.Key, item.Value);
|
||||
|
||||
socket.InvokeMessage(line.Substring(2));
|
||||
}
|
||||
else
|
||||
{
|
||||
// A update message from server to client
|
||||
var compareData = reader.ReadToEnd();
|
||||
foreach (var item in replaceValues)
|
||||
compareData = compareData.Replace(item.Key, item.Value);
|
||||
|
||||
socket.InvokeMessage(compareData);
|
||||
|
||||
await task.ConfigureAwait(false);
|
||||
object? result = task.Result.Data;
|
||||
if (responseMapper != null)
|
||||
result = responseMapper(task.Result.Data);
|
||||
|
||||
if (!skipResponseValidation)
|
||||
SystemTextJsonComparer.CompareData(name, result, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||
}
|
||||
}
|
||||
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ namespace CryptoExchange.Net.Testing
|
||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||
/// <param name="useFirstUpdateItem">Use the first item of an array update</param>
|
||||
/// <param name="addressPath">Path</param>
|
||||
/// <param name="skipUpdateValidation">Whether to skip comparing the json model with the update model</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task ValidateAsync<TUpdate>(
|
||||
@@ -57,7 +58,8 @@ namespace CryptoExchange.Net.Testing
|
||||
string? nestedJsonProperty = null,
|
||||
List<string>? ignoreProperties = null,
|
||||
string? addressPath = null,
|
||||
bool? useFirstUpdateItem = null)
|
||||
bool? useFirstUpdateItem = null,
|
||||
bool? skipUpdateValidation = null)
|
||||
{
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
@@ -92,10 +94,16 @@ namespace CryptoExchange.Net.Testing
|
||||
|
||||
TUpdate? update = default;
|
||||
// Invoke subscription method
|
||||
var task = methodInvoke(_client, x => { update = x.Data; });
|
||||
try
|
||||
{
|
||||
var task = methodInvoke(_client, x => { update = x.Data; });
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
string? overrideKey = null;
|
||||
string? overrideValue = null;
|
||||
var replaceValues = new Dictionary<string, string>();
|
||||
while (true)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
@@ -116,18 +124,36 @@ namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
foreach (var item in expectedJson.EnumerateObject())
|
||||
{
|
||||
if (item.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var innerItem in item.Value.EnumerateObject())
|
||||
{
|
||||
if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|"))
|
||||
{
|
||||
// |x| values are used to replace parts of response messages
|
||||
if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop))
|
||||
continue;
|
||||
|
||||
replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|"))
|
||||
{
|
||||
// |x| values are used to replace parts or response messages
|
||||
overrideKey = item.Value.ToString();
|
||||
var prop = lastMessageJson.GetProperty(item.Name);
|
||||
overrideValue = prop.ValueKind == JsonValueKind.String ? prop.GetString() : prop.GetInt64().ToString();
|
||||
// |x| values are used to replace parts of response messages
|
||||
if (!lastMessageJson.TryGetProperty(item.Name, out var prop))
|
||||
continue;
|
||||
|
||||
replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!);
|
||||
}
|
||||
else if (item.Value.ToString() == "-999")
|
||||
{
|
||||
// -999 value is used to replace parts or response messages
|
||||
overrideKey = item.Value.ToString();
|
||||
overrideValue = lastMessageJson.GetProperty(item.Name).GetDecimal().ToString();
|
||||
// |x| values are used to replace parts of response messages
|
||||
if (!lastMessageJson.TryGetProperty(item.Name, out var prop))
|
||||
continue;
|
||||
|
||||
replaceValues.Add(item.Value.ToString(), prop.GetDecimal().ToString()!);
|
||||
}
|
||||
else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true)
|
||||
{
|
||||
@@ -146,12 +172,8 @@ namespace CryptoExchange.Net.Testing
|
||||
else if (line.StartsWith("< "))
|
||||
{
|
||||
// Expect a message from server to client
|
||||
if (overrideKey != null)
|
||||
{
|
||||
line = line.Replace(overrideKey, overrideValue);
|
||||
overrideKey = null;
|
||||
overrideValue = null;
|
||||
}
|
||||
foreach (var item in replaceValues)
|
||||
line = line.Replace(item.Key, item.Value);
|
||||
|
||||
socket.InvokeMessage(line.Substring(2));
|
||||
}
|
||||
@@ -159,12 +181,16 @@ namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
// A update message from server to client
|
||||
var compareData = reader.ReadToEnd();
|
||||
foreach (var item in replaceValues)
|
||||
compareData = compareData.Replace(item.Key, item.Value);
|
||||
|
||||
socket.InvokeMessage(compareData);
|
||||
|
||||
if (update == null)
|
||||
throw new Exception($"{name} Update send to client did not trigger in update handler");
|
||||
|
||||
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||
if (skipUpdateValidation != true)
|
||||
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Testing.Implementations;
|
||||
|
||||
#pragma warning disable IL2026
|
||||
#pragma warning disable IL2070
|
||||
#pragma warning disable IL2075
|
||||
#pragma warning disable IL3050
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
var startResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!startResult)
|
||||
{
|
||||
_logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.ToString());
|
||||
_logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.Message, startResult.Error.Exception);
|
||||
Status = SyncStatus.Disconnected;
|
||||
return new CallResult(startResult.Error!);
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
var subResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.ToString());
|
||||
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception);
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
@@ -5,26 +5,28 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="10.18.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="8.1.1" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.14.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="4.3.2" />
|
||||
<PackageReference Include="CoinEx.Net" Version="8.0.1" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.6.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="1.0.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.20.1" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="1.1.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="1.21.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.21.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.15.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="1.1.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.8.1" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.9.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.7.1" />
|
||||
<PackageReference Include="Kucoin.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Binance.Net" Version="11.1.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.1.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.1.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.1.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.1.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.1.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="2.1.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.1.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="2.1.1" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.1.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="1.4.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="1.0.1" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="2.1.0" />
|
||||
<PackageReference Include="XT.Net" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
@inject IMexcRestClient mexcClient
|
||||
@inject IOKXRestClient okxClient
|
||||
@inject IToobitRestClient toobitClient
|
||||
@inject IWhiteBitRestClient whitebitClient
|
||||
@inject IXTRestClient xtClient
|
||||
|
||||
<h3>BTC-USD prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -33,7 +35,7 @@
|
||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
|
||||
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
|
||||
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
|
||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||
@@ -47,8 +49,10 @@
|
||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
|
||||
|
||||
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
|
||||
@@ -62,10 +66,10 @@
|
||||
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
|
||||
|
||||
if (bitgetTask.Result.Success)
|
||||
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
|
||||
_prices.Add("Bitget", bitgetTask.Result.Data.Single().LastPrice);
|
||||
|
||||
if (bitmartTask.Result.Success)
|
||||
_prices.Add("BitMart", bitgetTask.Result.Data.ClosePrice);
|
||||
_prices.Add("BitMart", bitmartTask.Result.Data.LastPrice);
|
||||
|
||||
if (bitmexTask.Result.Success)
|
||||
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
|
||||
@@ -114,11 +118,17 @@
|
||||
if (okxTask.Result.Success)
|
||||
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (toobitTask.Result.Success)
|
||||
_prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0);
|
||||
|
||||
if (whitebitTask.Result.Success){
|
||||
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
|
||||
var tickers = whitebitTask.Result.Data;
|
||||
_prices.Add("WhiteBit", tickers.Single(x => x.Symbol == "BTC_USDT").LastPrice);
|
||||
}
|
||||
|
||||
if (xtTask.Result.Success)
|
||||
_prices.Add("XT", xtTask.Result.Data.Single().LastPrice ?? 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,11 +17,14 @@
|
||||
@inject IKucoinSocketClient kucoinSocketClient
|
||||
@inject IMexcSocketClient mexcSocketClient
|
||||
@inject IOKXSocketClient okxSocketClient
|
||||
@inject IToobitSocketClient toobitSocketClient
|
||||
@inject IWhiteBitSocketClient whitebitSocketClient
|
||||
@inject IXTSocketClient xtSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
@using CryptoExchange.Net.Objects
|
||||
@using CryptoExchange.Net.Objects.Sockets;
|
||||
@using CryptoExchange.Net.Sockets
|
||||
@using XT.Net.Interfaces.Clients
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC prices, live updates:</h3>
|
||||
@@ -41,7 +44,7 @@
|
||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
||||
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
||||
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
@@ -51,12 +54,15 @@
|
||||
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
|
||||
gateioSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("GateIo", data.Data.LastPrice)),
|
||||
htxSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("HTX", data.Data.ClosePrice ?? 0)),
|
||||
xtSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("XT", data.Data.LastPrice ?? 0)),
|
||||
// HyperLiquid doesn't support the ETH/BTC pair
|
||||
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
|
||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
||||
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
|
||||
// Toobit doesn't support the ETH/BTC pair
|
||||
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
|
||||
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Authentication
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using CryptoCom.Net.Interfaces
|
||||
@using DeepCoin.Net.Interfaces
|
||||
@@ -21,7 +22,9 @@
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IBinanceOrderBookFactory binanceFactory
|
||||
@inject IBingXOrderBookFactory bingXFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@@ -40,7 +43,9 @@
|
||||
@inject IKucoinOrderBookFactory kucoinFactory
|
||||
@inject IMexcOrderBookFactory mexcFactory
|
||||
@inject IOKXOrderBookFactory okxFactory
|
||||
@inject IToobitOrderBookFactory toobitFactory
|
||||
@inject IWhiteBitOrderBookFactory whitebitFactory
|
||||
@inject IXTOrderBookFactory xtFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC books, live updates:</h3>
|
||||
@@ -69,7 +74,7 @@
|
||||
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
|
||||
KucoinRestClient.SetDefaultOptions(options =>
|
||||
{
|
||||
options.ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE");
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET", "PASSPHRASE");
|
||||
});
|
||||
|
||||
_books = new Dictionary<string, ISymbolOrderBook>
|
||||
@@ -85,7 +90,8 @@
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||
{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
|
||||
// DeepCoin does not support the ETH/BTC pair
|
||||
//{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
|
||||
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
||||
// HyperLiquid does not support the ETH/BTC pair
|
||||
//{ "HyperLiquid", hyperLiquidFactory.Create("ETH/BTC") },
|
||||
@@ -93,10 +99,13 @@
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
// Toobit does not support the ETH/BTC pair
|
||||
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
|
||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||
{ "XT", xtFactory.CreateSpot("eth_btc") },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
var result = await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
|
||||
// Use a manual update timer so the page isn't refreshed too often
|
||||
_timer = new Timer(500);
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IBinanceTrackerFactory binanceFactory
|
||||
@inject IBingXTrackerFactory bingXFactory
|
||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||
@@ -42,7 +44,9 @@
|
||||
@inject IKucoinTrackerFactory kucoinFactory
|
||||
@inject IMexcTrackerFactory mexcFactory
|
||||
@inject IOKXTrackerFactory okxFactory
|
||||
@inject IToobitTrackerFactory toobitFactory
|
||||
@inject IWhiteBitTrackerFactory whitebitFactory
|
||||
@inject IXTTrackerFactory xtFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC trade Trackers, live updates:</h3>
|
||||
@@ -88,7 +92,9 @@
|
||||
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user