mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afb84a1bf0 | |||
| 89cbd85875 | |||
| cc25a405c6 | |||
| d64c1171a3 | |||
| d4aded1ee2 | |||
| 271503c426 | |||
| f0ece589f7 | |||
| 3a9382ca0f | |||
| 1e1a02324a |
@@ -1,530 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using ProtoBuf;
|
|
||||||
using ProtoBuf.Meta;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices.ComTypes;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.Protobuf
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json message accessor
|
|
||||||
/// </summary>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public abstract class ProtobufMessageAccessor<
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
TIntermediateType> : IMessageAccessor
|
|
||||||
#else
|
|
||||||
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The intermediate deserialization object
|
|
||||||
/// </summary>
|
|
||||||
protected TIntermediateType? _intermediateType;
|
|
||||||
/// <summary>
|
|
||||||
/// Runtime type model
|
|
||||||
/// </summary>
|
|
||||||
protected RuntimeTypeModel _model;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => _intermediateType;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public ProtobufMessageAccessor(RuntimeTypeModel model)
|
|
||||||
{
|
|
||||||
_model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
|
||||||
{
|
|
||||||
if (_intermediateType == null)
|
|
||||||
throw new InvalidOperationException("Data not read");
|
|
||||||
|
|
||||||
object? value = _intermediateType;
|
|
||||||
foreach (var step in path)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (step.Type == 0)
|
|
||||||
{
|
|
||||||
// array index
|
|
||||||
}
|
|
||||||
else if (step.Type == 1)
|
|
||||||
{
|
|
||||||
// property value
|
|
||||||
#pragma warning disable IL2075 // Type is already annotated
|
|
||||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
|
||||||
#pragma warning restore
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// property name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var valueType = value.GetType();
|
|
||||||
if (valueType.IsArray)
|
|
||||||
return NodeType.Array;
|
|
||||||
|
|
||||||
if (IsSimple(valueType))
|
|
||||||
return NodeType.Value;
|
|
||||||
|
|
||||||
return NodeType.Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSimple(Type type)
|
|
||||||
{
|
|
||||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
||||||
{
|
|
||||||
// nullable type, check if the nested type is simple.
|
|
||||||
return IsSimple(type.GetGenericArguments()[0]);
|
|
||||||
}
|
|
||||||
return type.IsPrimitive
|
|
||||||
|| type.IsEnum
|
|
||||||
|| type == typeof(string)
|
|
||||||
|| type == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T? GetValue<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (_intermediateType == null)
|
|
||||||
throw new InvalidOperationException("Data not read");
|
|
||||||
|
|
||||||
object? value = _intermediateType;
|
|
||||||
foreach(var step in path)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (step.Type == 0)
|
|
||||||
{
|
|
||||||
// array index
|
|
||||||
}
|
|
||||||
else if (step.Type == 1)
|
|
||||||
{
|
|
||||||
// property value
|
|
||||||
#pragma warning disable IL2075 // Type is already annotated
|
|
||||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
|
||||||
#pragma warning restore
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// property name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T?)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public T?[]? GetValues<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public abstract CallResult<object> Deserialize(
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
Type type, MessagePath? path = null);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public abstract CallResult<T> Deserialize<
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
T>(MessagePath? path = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class ProtobufStreamMessageAccessor<
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override CallResult<object> Deserialize(
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _model.Deserialize(type, _stream);
|
|
||||||
return new CallResult<object>(result);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override CallResult<T> Deserialize<
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _model.Deserialize<T>(_stream);
|
|
||||||
return new CallResult<T>(result);
|
|
||||||
}
|
|
||||||
catch(Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<CallResult> Read(Stream stream, bool bufferStream)
|
|
||||||
{
|
|
||||||
if (bufferStream && stream is not MemoryStream)
|
|
||||||
{
|
|
||||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
|
||||||
_stream = new MemoryStream();
|
|
||||||
stream.CopyTo(_stream);
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
|
||||||
else if (bufferStream)
|
|
||||||
{
|
|
||||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
|
||||||
_stream = stream;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
|
|
||||||
IsValid = true;
|
|
||||||
return Task.FromResult(CallResult.SuccessResult);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsValid = false;
|
|
||||||
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString()
|
|
||||||
{
|
|
||||||
if (_stream is null)
|
|
||||||
throw new NullReferenceException("Stream not initialized");
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
|
||||||
return textReader.ReadToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_stream?.Dispose();
|
|
||||||
_stream = null;
|
|
||||||
_intermediateType = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Protobuf byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class ProtobufByteMessageAccessor<
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
|
|
||||||
{
|
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override CallResult<object> Deserialize(
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
#endif
|
|
||||||
Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = new MemoryStream(_bytes.ToArray());
|
|
||||||
stream.Position = 0;
|
|
||||||
var result = _model.Deserialize(type, stream);
|
|
||||||
return new CallResult<object>(result);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public override CallResult<T> Deserialize<
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[DynamicallyAccessedMembers(
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
|
||||||
#endif
|
|
||||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods
|
|
||||||
)]
|
|
||||||
T>(MessagePath? path = null)
|
|
||||||
#else
|
|
||||||
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _model.Deserialize<T>(_bytes);
|
|
||||||
return new CallResult<T>(result);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
_bytes = data;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_intermediateType = _model.Deserialize<TIntermediate>(data);
|
|
||||||
IsValid = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString() =>
|
|
||||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
|
||||||
#if NETSTANDARD2_0
|
|
||||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
|
||||||
#else
|
|
||||||
Encoding.UTF8.GetString(_bytes.Span);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_bytes = null;
|
|
||||||
_intermediateType = default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
|
||||||
<Authors>JKorf</Authors>
|
|
||||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
|
||||||
<PackageVersion>10.0.1</PackageVersion>
|
|
||||||
<AssemblyVersion>10.0.1</AssemblyVersion>
|
|
||||||
<FileVersion>10.0.1</FileVersion>
|
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
|
||||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
|
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
|
||||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
|
||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
|
||||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#  CryptoExchange.Net.Proto
|
|
||||||
|
|
||||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
|
||||||
|
|
||||||
Protobuf support for CryptoExchange.Net.
|
|
||||||
|
|
||||||
## Release notes
|
|
||||||
* Version 10.0.1 - 16 Dec 2025
|
|
||||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 10.0.0 - 16 Dec 2025
|
|
||||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.13.0 - 10 Nov 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.12.0 - 03 Nov 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.11.1 - 30 Oct 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.11.0 - 30 Oct 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.10.0 - 15 Oct 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.9.0 - 06 Oct 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.8.0 - 30 Sep 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.7.0 - 01 Sep 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.6.0 - 25 Aug 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.6.0
|
|
||||||
|
|
||||||
* Version 9.5.0 - 19 Aug 2025
|
|
||||||
* Updated CryptoExchange.Net version to 9.5.0
|
|
||||||
|
|
||||||
* Version 9.4.0 - 04 Aug 2025
|
|
||||||
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
* Updated protobuf-net package version to 3.2.56
|
|
||||||
|
|
||||||
* Version 9.3.0 - 23 Jul 2025
|
|
||||||
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
|
||||||
|
|
||||||
* Version 9.2.0 - 14 Jul 2025
|
|
||||||
* Initial release
|
|
||||||
@@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -43,10 +41,6 @@ Global
|
|||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>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>11.2.0</PackageVersion>
|
<PackageVersion>11.2.2</PackageVersion>
|
||||||
<AssemblyVersion>11.2.0</AssemblyVersion>
|
<AssemblyVersion>11.2.2</AssemblyVersion>
|
||||||
<FileVersion>11.2.0</FileVersion>
|
<FileVersion>11.2.2</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
var nextEndTime = lastTimestamp.AddSeconds(-(int)request.Interval);
|
var nextEndTime = lastTimestamp.AddSeconds(-(int)request.Interval);
|
||||||
var startTime = nextEndTime.AddSeconds(-(limit * (int)request.Interval));
|
var startTime = nextEndTime.AddSeconds(-(limit * (int)request.Interval));
|
||||||
var requestStartTime = request.StartTime ?? DateTime.UtcNow;
|
var requestStartTime = request.StartTime ?? default(DateTime);
|
||||||
if (startTime < requestStartTime)
|
if (startTime < requestStartTime)
|
||||||
startTime = requestStartTime;
|
startTime = requestStartTime;
|
||||||
return new PageRequest { StartTime = startTime, EndTime = nextEndTime };
|
return new PageRequest { StartTime = startTime, EndTime = nextEndTime };
|
||||||
|
|||||||
@@ -584,11 +584,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
|
byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
|
||||||
var buffer = new ArraySegment<byte>(rentedBuffer);
|
var buffer = new ArraySegment<byte>(rentedBuffer);
|
||||||
|
var broken = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested || broken)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
MemoryStream? multipartStream = null;
|
MemoryStream? multipartStream = null;
|
||||||
@@ -622,6 +623,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
|
|
||||||
|
broken = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,11 +738,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
|
byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
|
||||||
var buffer = new Memory<byte>(rentedBuffer);
|
var buffer = new Memory<byte>(rentedBuffer);
|
||||||
|
var broken = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested || broken)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
MemoryStream? multipartStream = null;
|
MemoryStream? multipartStream = null;
|
||||||
@@ -773,6 +777,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
|
|
||||||
|
broken = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -395,9 +395,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
}
|
}
|
||||||
else if (objectValue is bool bl)
|
else if (objectValue is bool bl)
|
||||||
{
|
{
|
||||||
if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True"))
|
if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True" && stringValue != "yes" && stringValue != "YES"))
|
||||||
throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}");
|
throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}");
|
||||||
if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False"))
|
if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False" && stringValue != "no" && stringValue != "NO"))
|
||||||
throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}");
|
throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}");
|
||||||
}
|
}
|
||||||
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// <param name="name">Method name for looking up json test values</param>
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="ignoreParamValidation">Ignore certain parameter validation</param>
|
||||||
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -66,9 +67,10 @@ namespace CryptoExchange.Net.Testing
|
|||||||
string name,
|
string name,
|
||||||
string? nestedJsonProperty = null,
|
string? nestedJsonProperty = null,
|
||||||
List<string>? ignoreProperties = null,
|
List<string>? ignoreProperties = null,
|
||||||
|
List<string>? ignoreParamValidation = null,
|
||||||
bool useSingleArrayItem = false,
|
bool useSingleArrayItem = false,
|
||||||
bool skipResponseValidation = false)
|
bool skipResponseValidation = false)
|
||||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation);
|
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, ignoreParamValidation, useSingleArrayItem, skipResponseValidation);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate a request
|
/// Validate a request
|
||||||
@@ -79,6 +81,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// <param name="name">Method name for looking up json test values</param>
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="ignoreParamValidation">Ignore certain parameter validation</param>
|
||||||
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -88,6 +91,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
string name,
|
string name,
|
||||||
string? nestedJsonProperty = null,
|
string? nestedJsonProperty = null,
|
||||||
List<string>? ignoreProperties = null,
|
List<string>? ignoreProperties = null,
|
||||||
|
List<string>? ignoreParamValidation = null,
|
||||||
bool useSingleArrayItem = false,
|
bool useSingleArrayItem = false,
|
||||||
bool skipResponseValidation = false) where TActualResponse : TResponse
|
bool skipResponseValidation = false) where TActualResponse : TResponse
|
||||||
{
|
{
|
||||||
@@ -115,7 +119,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
var expectedPath = reader.ReadLine();
|
var expectedPath = reader.ReadLine();
|
||||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
var paramsAndResponseBody = reader.ReadToEnd();
|
var paramsAndResponseBody = reader.ReadToEnd();
|
||||||
var paramsAndResponseBodySplit = paramsAndResponseBody.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
|
var paramsAndResponseBodySplit = paramsAndResponseBody.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
var uriParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("UriParams: "));
|
var uriParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("UriParams: "));
|
||||||
Dictionary<string, object>? expectedUriParams = null;
|
Dictionary<string, object>? expectedUriParams = null;
|
||||||
@@ -156,7 +160,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
? urlParametersString.Split('&').ToDictionary(x => x.Split('=')[0], x => (object)x.Split('=')[1])
|
? urlParametersString.Split('&').ToDictionary(x => x.Split('=')[0], x => (object)x.Split('=')[1])
|
||||||
: new());
|
: new());
|
||||||
|
|
||||||
CompareParameters(expectedUriParams, urlParameters);
|
CompareParameters(expectedUriParams, urlParameters, ignoreParamValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedBodyParams != null)
|
if (expectedBodyParams != null)
|
||||||
@@ -173,7 +177,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]);
|
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
CompareParameters(expectedBodyParams, bodyParameters);
|
CompareParameters(expectedBodyParams, bodyParameters, ignoreParamValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipResponseValidation)
|
if (!skipResponseValidation)
|
||||||
@@ -186,13 +190,16 @@ namespace CryptoExchange.Net.Testing
|
|||||||
Trace.Listeners.Remove(listener);
|
Trace.Listeners.Remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CompareParameters(Dictionary<string, object> expectedUrlParameters, Dictionary<string, object> parameters)
|
private void CompareParameters(Dictionary<string, object> expectedUrlParameters, Dictionary<string, object> parameters, List<string>? ignoreParamValidation)
|
||||||
{
|
{
|
||||||
if (expectedUrlParameters.Count > parameters.Count)
|
if (expectedUrlParameters.Count > parameters.Count)
|
||||||
throw new Exception($"Url parameters count not matched. Expected: {expectedUrlParameters.Count}, Actual: {parameters.Count}");
|
throw new Exception($"Url parameters count not matched. Expected: {expectedUrlParameters.Count}, Actual: {parameters.Count}");
|
||||||
|
|
||||||
foreach (var kvp in expectedUrlParameters)
|
foreach (var kvp in expectedUrlParameters)
|
||||||
{
|
{
|
||||||
|
if (ignoreParamValidation != null && ignoreParamValidation.Contains(kvp.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!parameters.TryGetValue(kvp.Key, out var value))
|
if (!parameters.TryGetValue(kvp.Key, out var value))
|
||||||
throw new Exception($"Url parameter {kvp.Key} not found in actual parameters");
|
throw new Exception($"Url parameter {kvp.Key} not found in actual parameters");
|
||||||
|
|
||||||
@@ -206,11 +213,14 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="methodInvoke">Method invocation</param>
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
/// <param name="name">Method name for looking up json test values</param>
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="ignoreParamValidation">Ignore certain parameter validation</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="Exception"></exception>
|
/// <exception cref="Exception"></exception>
|
||||||
public async Task ValidateAsync(
|
public async Task ValidateAsync(
|
||||||
Func<TClient, Task<WebCallResult>> methodInvoke,
|
Func<TClient, Task<WebCallResult>> methodInvoke,
|
||||||
string name)
|
string name,
|
||||||
|
List<string>? ignoreParamValidation = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var listener = new EnumValueTraceListener();
|
var listener = new EnumValueTraceListener();
|
||||||
Trace.Listeners.Add(listener);
|
Trace.Listeners.Add(listener);
|
||||||
@@ -235,7 +245,26 @@ namespace CryptoExchange.Net.Testing
|
|||||||
var expectedMethod = reader.ReadLine();
|
var expectedMethod = reader.ReadLine();
|
||||||
var expectedPath = reader.ReadLine();
|
var expectedPath = reader.ReadLine();
|
||||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
var response = reader.ReadToEnd();
|
var paramsAndResponseBody = reader.ReadToEnd();
|
||||||
|
var paramsAndResponseBodySplit = paramsAndResponseBody.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
var uriParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("UriParams: "));
|
||||||
|
Dictionary<string, object>? expectedUriParams = null;
|
||||||
|
var bodyParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("BodyParams: "));
|
||||||
|
Dictionary<string, object>? expectedBodyParams = null;
|
||||||
|
var response = string.Join("\r\n", paramsAndResponseBodySplit.Where(x => !x.StartsWith("UriParams: ") && !x.StartsWith("BodyParams: ")));
|
||||||
|
|
||||||
|
if (uriParamsLine != null)
|
||||||
|
{
|
||||||
|
var expectedUriParamsJson = uriParamsLine.Substring(11);
|
||||||
|
expectedUriParams = JsonSerializer.Deserialize<Dictionary<string, object>>(expectedUriParamsJson)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyParamsLine != null)
|
||||||
|
{
|
||||||
|
var expectedBodyParamsJson = bodyParamsLine.Substring(12);
|
||||||
|
expectedBodyParams = JsonSerializer.Deserialize<Dictionary<string, object>>(expectedBodyParamsJson)!;
|
||||||
|
}
|
||||||
|
|
||||||
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||||
var result = await methodInvoke(_client).ConfigureAwait(false);
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
@@ -249,6 +278,35 @@ namespace CryptoExchange.Net.Testing
|
|||||||
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
if (expectedUriParams != null)
|
||||||
|
{
|
||||||
|
// Validate request parameters
|
||||||
|
var urlParamsSplit = result.RequestUrl!.Split(new char[] { '?' });
|
||||||
|
var urlParametersString = urlParamsSplit.Length > 1 ? urlParamsSplit[1] : null;
|
||||||
|
var urlParameters = (urlParametersString != null
|
||||||
|
? urlParametersString.Split('&').ToDictionary(x => x.Split('=')[0], x => (object)x.Split('=')[1])
|
||||||
|
: new());
|
||||||
|
|
||||||
|
CompareParameters(expectedUriParams, urlParameters, ignoreParamValidation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedBodyParams != null)
|
||||||
|
{
|
||||||
|
// Validate request body
|
||||||
|
Dictionary<string, object> bodyParameters;
|
||||||
|
if (result.RequestBody!.StartsWith("{") || result.RequestBody.StartsWith("["))
|
||||||
|
{
|
||||||
|
bodyParameters = JsonSerializer.Deserialize<Dictionary<string, object>>(result.RequestBody!)!;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var splitKvp = result.RequestBody.Split('&');
|
||||||
|
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompareParameters(expectedBodyParams, bodyParameters, ignoreParamValidation);
|
||||||
|
}
|
||||||
|
|
||||||
Trace.Listeners.Remove(listener);
|
Trace.Listeners.Remove(listener);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,33 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.11.3" />
|
<PackageReference Include="Binance.Net" Version="12.13.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="10.10.2" />
|
<PackageReference Include="Bitfinex.Net" Version="10.12.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.10.0" />
|
<PackageReference Include="BitMart.Net" Version="3.11.2" />
|
||||||
<PackageReference Include="BloFin.Net" Version="2.10.2" />
|
<PackageReference Include="BloFin.Net" Version="2.11.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="6.11.0" />
|
<PackageReference Include="Bybit.Net" Version="6.13.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="10.9.2" />
|
<PackageReference Include="CoinEx.Net" Version="10.11.0" />
|
||||||
<PackageReference Include="CoinW.Net" Version="2.9.2" />
|
<PackageReference Include="CoinW.Net" Version="2.10.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="3.10.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.11.0" />
|
||||||
<PackageReference Include="DeepCoin.Net" Version="3.9.2" />
|
<PackageReference Include="DeepCoin.Net" Version="3.10.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="3.10.2" />
|
<PackageReference Include="GateIo.Net" Version="3.11.0" />
|
||||||
<PackageReference Include="HyperLiquid.Net" Version="4.3.0" />
|
<PackageReference Include="HyperLiquid.Net" Version="4.7.1" />
|
||||||
<PackageReference Include="JK.BingX.Net" Version="3.10.0" />
|
<PackageReference Include="JK.BingX.Net" Version="3.11.1" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="3.10.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.12.1" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="5.0.1" />
|
<PackageReference Include="JK.Mexc.Net" Version="5.2.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.12.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.14.0" />
|
||||||
<PackageReference Include="Jkorf.Aster.Net" Version="3.1.0" />
|
<PackageReference Include="Jkorf.Aster.Net" Version="3.3.0" />
|
||||||
<PackageReference Include="JKorf.BitMEX.Net" Version="3.9.2" />
|
<PackageReference Include="JKorf.BitMEX.Net" Version="3.10.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.9.2" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.10.1" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="8.9.1" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.10.1" />
|
||||||
<PackageReference Include="JKorf.Upbit.Net" Version="2.9.2" />
|
<PackageReference Include="JKorf.Upbit.Net" Version="2.10.1" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="7.9.1" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.11.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="8.11.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.13.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Toobit.Net" Version="3.9.2" />
|
<PackageReference Include="Toobit.Net" Version="3.10.0" />
|
||||||
<PackageReference Include="Weex.Net" Version="1.0.0" />
|
<PackageReference Include="Weex.Net" Version="1.1.1" />
|
||||||
<PackageReference Include="WhiteBit.Net" Version="3.9.2" />
|
<PackageReference Include="WhiteBit.Net" Version="3.11.1" />
|
||||||
<PackageReference Include="XT.Net" Version="3.9.2" />
|
<PackageReference Include="XT.Net" Version="3.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -16,13 +16,26 @@
|
|||||||
{
|
{
|
||||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC");
|
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC");
|
||||||
var tasks = restClients.Select(x => x.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
var tasks = restClients.Select(x => x.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
||||||
await Task.WhenAll(tasks);
|
await foreach (var ticker in ParallelEnumerateAsync(tasks))
|
||||||
foreach (var ticker in tasks.Select(x => x.Result))
|
|
||||||
{
|
{
|
||||||
if (ticker.Success)
|
if (ticker.Success)
|
||||||
_prices.Add(ticker.Exchange, ticker.Data.LastPrice);
|
_prices.Add(ticker.Exchange, ticker.Data.LastPrice);
|
||||||
else
|
else
|
||||||
Debug.WriteLine($"{ticker.Exchange} failed: {ticker.Error}");
|
Debug.WriteLine($"{ticker.Exchange} failed: {ticker.Error}");
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async IAsyncEnumerable<T> ParallelEnumerateAsync<T>(IEnumerable<Task<T>> tasks)
|
||||||
|
{
|
||||||
|
var remaining = new List<Task<T>>(tasks);
|
||||||
|
|
||||||
|
while (remaining.Count != 0)
|
||||||
|
{
|
||||||
|
var task = await Task.WhenAny(remaining).ConfigureAwait(false);
|
||||||
|
remaining.Remove(task);
|
||||||
|
yield return task.Result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,13 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.11.0" />
|
<PackageReference Include="Binance.Net" Version="12.13.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="10.10.1" />
|
<PackageReference Include="Bybit.Net" Version="6.13.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.9.1" />
|
</ItemGroup>
|
||||||
<PackageReference Include="Bybit.Net" Version="6.10.0" />
|
|
||||||
<PackageReference Include="CoinEx.Net" Version="10.9.1" />
|
|
||||||
<PackageReference Include="CryptoCom.Net" Version="3.9.1" />
|
|
||||||
<PackageReference Include="GateIo.Net" Version="3.10.1" />
|
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="3.9.0" />
|
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="4.9.0" />
|
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.10.1" />
|
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.9.1" />
|
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="8.9.0" />
|
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="7.9.0" />
|
|
||||||
<PackageReference Include="Kucoin.Net" Version="8.10.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Binance.Net.Clients;
|
|
||||||
using Binance.Net.Interfaces.Clients;
|
|
||||||
using ConsoleClient.Models;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
|
|
||||||
namespace ConsoleClient.Exchanges
|
|
||||||
{
|
|
||||||
internal class BinanceExchange : IExchange
|
|
||||||
{
|
|
||||||
private IBinanceSocketClient _socketClient = new BinanceSocketClient();
|
|
||||||
|
|
||||||
public async Task<WebCallResult> CancelOrder(string symbol, string id)
|
|
||||||
{
|
|
||||||
using var client = new BinanceRestClient();
|
|
||||||
var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id));
|
|
||||||
return result.AsDataless();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, decimal>> GetBalances()
|
|
||||||
{
|
|
||||||
using var client = new BinanceRestClient();
|
|
||||||
var result = await client.SpotApi.Account.GetAccountInfoAsync();
|
|
||||||
return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
|
||||||
{
|
|
||||||
using var client = new BinanceRestClient();
|
|
||||||
var result = await client.SpotApi.Trading.GetOpenOrdersAsync();
|
|
||||||
// Should check result success status here
|
|
||||||
return result.Data.Select(o => new OpenOrder
|
|
||||||
{
|
|
||||||
Symbol = o.Symbol,
|
|
||||||
OrderSide = o.Side.ToString(),
|
|
||||||
OrderStatus = o.Status.ToString(),
|
|
||||||
OrderTime = o.CreateTime,
|
|
||||||
OrderType = o.Type.ToString(),
|
|
||||||
Price = o.Price,
|
|
||||||
Quantity = o.Quantity,
|
|
||||||
QuantityFilled = o.QuantityFilled
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<decimal> GetPrice(string symbol)
|
|
||||||
{
|
|
||||||
using var client = new BinanceRestClient();
|
|
||||||
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
|
|
||||||
// Should check result success status here
|
|
||||||
return result.Data.Price;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
|
|
||||||
{
|
|
||||||
using var client = new BinanceRestClient();
|
|
||||||
var result = await client.SpotApi.Trading.PlaceOrderAsync(
|
|
||||||
symbol,
|
|
||||||
side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell,
|
|
||||||
type == "market" ? Binance.Net.Enums.SpotOrderType.Market : Binance.Net.Enums.SpotOrderType.Limit,
|
|
||||||
quantity,
|
|
||||||
price: price,
|
|
||||||
timeInForce: type == "market" ? null: Binance.Net.Enums.TimeInForce.GoodTillCanceled);
|
|
||||||
return result.As(result.Data?.Id.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
|
|
||||||
{
|
|
||||||
var sub = await _socketClient.SpotApi.ExchangeData.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
|
|
||||||
return sub.Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
using Bybit.Net.Clients;
|
|
||||||
using Bybit.Net.Interfaces.Clients;
|
|
||||||
using ConsoleClient.Models;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ConsoleClient.Exchanges
|
|
||||||
{
|
|
||||||
internal class BybitExchange : IExchange
|
|
||||||
{
|
|
||||||
private IBybitSocketClient _socketClient = new BybitSocketClient();
|
|
||||||
|
|
||||||
public async Task<WebCallResult> CancelOrder(string symbol, string id)
|
|
||||||
{
|
|
||||||
using var client = new BybitRestClient();
|
|
||||||
var result = await client.V5Api.Trading.CancelOrderAsync(Bybit.Net.Enums.Category.Spot, symbol, id);
|
|
||||||
return result.AsDataless();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, decimal>> GetBalances()
|
|
||||||
{
|
|
||||||
using var client = new BybitRestClient();
|
|
||||||
var result = await client.V5Api.Account.GetBalancesAsync(Bybit.Net.Enums.AccountType.Spot);
|
|
||||||
return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
|
||||||
{
|
|
||||||
using var client = new BybitRestClient();
|
|
||||||
var order = await client.V5Api.Trading.GetOrdersAsync(Bybit.Net.Enums.Category.Spot);
|
|
||||||
return order.Data.List.Select(o => new OpenOrder
|
|
||||||
{
|
|
||||||
Symbol = o.Symbol,
|
|
||||||
OrderSide = o.Side.ToString(),
|
|
||||||
OrderStatus = o.Status.ToString(),
|
|
||||||
OrderTime = o.CreateTime,
|
|
||||||
OrderType = o.OrderType.ToString(),
|
|
||||||
Price = o.Price ?? 0,
|
|
||||||
Quantity = o.Quantity,
|
|
||||||
QuantityFilled = o.QuantityFilled ?? 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<decimal> GetPrice(string symbol)
|
|
||||||
{
|
|
||||||
using var client = new BybitRestClient();
|
|
||||||
var result = await client.V5Api.ExchangeData.GetSpotTickersAsync(symbol);
|
|
||||||
return result.Data.List.First().LastPrice;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
|
|
||||||
{
|
|
||||||
using var client = new BybitRestClient();
|
|
||||||
var result = await client.V5Api.Trading.PlaceOrderAsync(
|
|
||||||
Bybit.Net.Enums.Category.Spot,
|
|
||||||
symbol,
|
|
||||||
side.ToLower() == "buy" ? Bybit.Net.Enums.OrderSide.Buy : Bybit.Net.Enums.OrderSide.Sell,
|
|
||||||
type == "market" ? Bybit.Net.Enums.NewOrderType.Market : Bybit.Net.Enums.NewOrderType.Limit,
|
|
||||||
quantity,
|
|
||||||
price: price);
|
|
||||||
return result.As(result.Data?.OrderId.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
|
|
||||||
{
|
|
||||||
var sub = await _socketClient.V5SpotApi.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
|
|
||||||
return sub.Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using ConsoleClient.Models;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ConsoleClient.Exchanges
|
|
||||||
{
|
|
||||||
public interface IExchange
|
|
||||||
{
|
|
||||||
Task<decimal> GetPrice(string symbol);
|
|
||||||
Task<IEnumerable<OpenOrder>> GetOpenOrders();
|
|
||||||
Task<Dictionary<string, decimal>> GetBalances();
|
|
||||||
Task<WebCallResult> CancelOrder(string symbol, string id);
|
|
||||||
Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price );
|
|
||||||
Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ConsoleClient.Models
|
|
||||||
{
|
|
||||||
public class OpenOrder
|
|
||||||
{
|
|
||||||
public string Symbol { get; set; }
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
public decimal QuantityFilled { get; set; }
|
|
||||||
public string OrderType { get; set; }
|
|
||||||
public string OrderStatus { get; set; }
|
|
||||||
public string OrderSide { get; set; }
|
|
||||||
public DateTime OrderTime { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+292
-153
@@ -1,164 +1,303 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Binance.Net;
|
using Binance.Net;
|
||||||
using Binance.Net.Clients;
|
using Binance.Net.Clients;
|
||||||
using Bybit.Net;
|
using Bybit.Net;
|
||||||
using Bybit.Net.Clients;
|
using Bybit.Net.Clients;
|
||||||
using ConsoleClient.Exchanges;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
|
|
||||||
namespace ConsoleClient
|
var binanceRest = new BinanceRestClient(options =>
|
||||||
{
|
{
|
||||||
internal class Program
|
options.ApiCredentials = new BinanceCredentials("KEY", "SECRET");
|
||||||
|
});
|
||||||
|
var bybitRest = new BybitRestClient(options =>
|
||||||
|
{
|
||||||
|
options.ApiCredentials = new BybitCredentials("KEY", "SECRET");
|
||||||
|
});
|
||||||
|
var binanceSocket = new BinanceSocketClient(options =>
|
||||||
|
{
|
||||||
|
options.ApiCredentials = new BinanceCredentials("KEY", "SECRET");
|
||||||
|
});
|
||||||
|
var bybitSocket = new BybitSocketClient(options =>
|
||||||
|
{
|
||||||
|
options.ApiCredentials = new BybitCredentials("KEY", "SECRET");
|
||||||
|
});
|
||||||
|
|
||||||
|
var exchanges = new Dictionary<string, ExchangeClients>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["binance"] = new ExchangeClients(
|
||||||
|
"Binance",
|
||||||
|
binanceRest.SpotApi.SharedClient,
|
||||||
|
binanceRest.SpotApi.SharedClient,
|
||||||
|
binanceRest.SpotApi.SharedClient,
|
||||||
|
binanceRest.SpotApi.SharedClient,
|
||||||
|
binanceSocket.SpotApi.SharedClient),
|
||||||
|
["bybit"] = new ExchangeClients(
|
||||||
|
"Bybit",
|
||||||
|
bybitRest.V5Api.SharedClient,
|
||||||
|
bybitRest.V5Api.SharedClient,
|
||||||
|
bybitRest.V5Api.SharedClient,
|
||||||
|
bybitRest.V5Api.SharedClient,
|
||||||
|
bybitSocket.V5SpotApi.SharedClient)
|
||||||
|
};
|
||||||
|
|
||||||
|
PrintHelp();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Console.Write("> ");
|
||||||
|
var input = Console.ReadLine();
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var commandArgs = input.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
var command = commandArgs[0].ToLowerInvariant();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
|
switch (command)
|
||||||
{
|
{
|
||||||
{ "Binance", new Exchanges.BinanceExchange() },
|
case "help":
|
||||||
{ "Bybit", new Exchanges.BybitExchange() }
|
PrintHelp();
|
||||||
};
|
break;
|
||||||
|
case "exit":
|
||||||
static async Task Main(string[] args)
|
case "quit":
|
||||||
{
|
return;
|
||||||
BinanceRestClient.SetDefaultOptions(options =>
|
case "prices":
|
||||||
{
|
await PrintPricesAsync(commandArgs);
|
||||||
options.ApiCredentials = new BinanceCredentials("APIKEY", "APISECRET");
|
break;
|
||||||
});
|
case "stream":
|
||||||
BybitRestClient.SetDefaultOptions(options =>
|
await StreamTickerAsync(commandArgs);
|
||||||
{
|
break;
|
||||||
options.ApiCredentials = new BybitCredentials("APIKEY", "APISECRET");
|
case "orderbook":
|
||||||
});
|
await PrintOrderBookAsync(commandArgs);
|
||||||
|
break;
|
||||||
while (true)
|
case "balances":
|
||||||
{
|
await PrintBalancesAsync(commandArgs);
|
||||||
Console.WriteLine("> Available commands: PlaceOrder, GetBalances, GetOpenOrders, CancelOrder, GetPrice, SubscribePrice");
|
break;
|
||||||
var input = Console.ReadLine();
|
case "open-orders":
|
||||||
|
await PrintOpenOrdersAsync(commandArgs);
|
||||||
switch (input)
|
break;
|
||||||
{
|
case "place-limit":
|
||||||
case "PlaceOrder":
|
await PlaceLimitOrderAsync(commandArgs);
|
||||||
await ProcessPlaceOrder();
|
break;
|
||||||
break;
|
case "cancel-order":
|
||||||
case "GetBalances":
|
await CancelOrderAsync(commandArgs);
|
||||||
await ProcessGetBalances();
|
break;
|
||||||
break;
|
default:
|
||||||
case "GetOpenOrders":
|
Console.WriteLine("Unknown command. Type `help` for examples.");
|
||||||
await ProcessGetOpenOrders();
|
break;
|
||||||
break;
|
|
||||||
case "CancelOrder":
|
|
||||||
await ProcessCancelOrder();
|
|
||||||
break;
|
|
||||||
case "GetPrice":
|
|
||||||
await ProcessGetPrice();
|
|
||||||
break;
|
|
||||||
case "SubscribePrice":
|
|
||||||
var sub = await ProcessSubscribePrice();
|
|
||||||
Console.ReadLine();
|
|
||||||
await sub.CloseAsync();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Console.WriteLine("> Unknown command");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task ProcessPlaceOrder()
|
|
||||||
{
|
|
||||||
var exchange = GetInput("Exchange?");
|
|
||||||
var symbol = GetInput("Symbol?");
|
|
||||||
var side = GetInput("Buy/Sell?");
|
|
||||||
var type = GetInput("Limit/Market?");
|
|
||||||
var quantity = GetDecimalInput("Quantity?");
|
|
||||||
decimal? price = null;
|
|
||||||
if (type != "Market")
|
|
||||||
price = GetDecimalInput("Price?");
|
|
||||||
var result = await _exchanges[exchange].PlaceOrder(symbol, side, type, quantity, price);
|
|
||||||
if (result.Success)
|
|
||||||
Console.WriteLine("Order placed, ID: " + result.Data);
|
|
||||||
else
|
|
||||||
Console.WriteLine("Failed to place order: " + result.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task ProcessGetBalances()
|
|
||||||
{
|
|
||||||
var exchange = GetInput("Exchange?");
|
|
||||||
Console.WriteLine("Requesting balances...");
|
|
||||||
var balances = await _exchanges[exchange].GetBalances();
|
|
||||||
foreach (var balance in balances.Where(b => b.Value != 0))
|
|
||||||
Console.WriteLine($"> {balance.Key}: {balance.Value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task ProcessGetOpenOrders()
|
|
||||||
{
|
|
||||||
var exchange = GetInput("Exchange?");
|
|
||||||
Console.WriteLine("Requesting open orders...");
|
|
||||||
var orders = await _exchanges[exchange].GetOpenOrders();
|
|
||||||
if (!orders.Any())
|
|
||||||
Console.WriteLine("No open orders found");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach(var openOrder in orders)
|
|
||||||
Console.WriteLine($"> {openOrder.OrderTime} - {openOrder.Symbol} {openOrder.OrderType} {openOrder.OrderSide} {openOrder.QuantityFilled}/{openOrder.Quantity} @ {openOrder.Price}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task ProcessCancelOrder()
|
|
||||||
{
|
|
||||||
var exchange = GetInput("Exchange?");
|
|
||||||
var symbol = GetInput("Symbol?");
|
|
||||||
var id = GetInput("Order id?");
|
|
||||||
var result = await _exchanges[exchange].CancelOrder(symbol, id);
|
|
||||||
if (result)
|
|
||||||
Console.WriteLine("Order canceled");
|
|
||||||
else
|
|
||||||
Console.WriteLine("Order cancel failed: " + result.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task ProcessGetPrice()
|
|
||||||
{
|
|
||||||
var exchange = GetInput("Exchange?");
|
|
||||||
var symbol = GetInput("Symbol?");
|
|
||||||
Console.WriteLine("Requesting price...");
|
|
||||||
var price = await _exchanges[exchange].GetPrice(symbol);
|
|
||||||
Console.WriteLine($"> {exchange} price for {symbol}: {price}");
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task<UpdateSubscription> ProcessSubscribePrice()
|
|
||||||
{
|
|
||||||
var exchange = GetInput("Exchange?");
|
|
||||||
var symbol = GetInput("Symbol?");
|
|
||||||
Console.WriteLine("Subscribing price...");
|
|
||||||
return await _exchanges[exchange].SubscribePrice(symbol, price => HandlePriceUpdate(exchange,symbol, price));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void HandlePriceUpdate(string exchange, string symbol, decimal price)
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine($"> {exchange} price for {symbol}: {price}");
|
|
||||||
Console.WriteLine("Press enter to stop live price updates");
|
|
||||||
}
|
|
||||||
|
|
||||||
static decimal GetDecimalInput(string question)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
Console.WriteLine("> " + question);
|
|
||||||
var response = Console.ReadLine();
|
|
||||||
if (decimal.TryParse(response, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var answer))
|
|
||||||
return answer;
|
|
||||||
else
|
|
||||||
Console.WriteLine("Invalid decimal value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static string GetInput(string question)
|
|
||||||
{
|
|
||||||
Console.WriteLine("> " + question);
|
|
||||||
return Console.ReadLine();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Command failed: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task PrintPricesAsync(string[] args)
|
||||||
|
{
|
||||||
|
var symbol = GetSymbol(args, 1);
|
||||||
|
var tasks = exchanges.Values.Select(async exchange =>
|
||||||
|
{
|
||||||
|
var result = await exchange.TickerRest.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{exchange.Name,-8} error: {result.Error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"{exchange.Name,-8} {result.Data.Symbol,-12} last={result.Data.LastPrice}");
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task StreamTickerAsync(string[] args)
|
||||||
|
{
|
||||||
|
var exchange = GetExchange(args, 1);
|
||||||
|
var symbol = GetSymbol(args, 2);
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
var result = await exchange.TickerSocket.SubscribeToTickerUpdatesAsync(
|
||||||
|
new SubscribeTickerRequest(symbol),
|
||||||
|
update => Console.WriteLine($"{exchange.Name,-8} {update.Data.Symbol,-12} last={update.Data.LastPrice}"),
|
||||||
|
cts.Token);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Subscription failed: {result.Error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Streaming ticker updates. Press Enter to stop.");
|
||||||
|
Console.ReadLine();
|
||||||
|
cts.Cancel();
|
||||||
|
await result.Data.CloseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task PrintOrderBookAsync(string[] args)
|
||||||
|
{
|
||||||
|
var exchange = GetExchange(args, 1);
|
||||||
|
var symbol = GetSymbol(args, 2);
|
||||||
|
|
||||||
|
var result = await exchange.OrderBookRest.GetOrderBookAsync(new GetOrderBookRequest(symbol, limit: 5));
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Order book request failed: {result.Error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"{exchange.Name} {symbol.BaseAsset}/{symbol.QuoteAsset} top of book");
|
||||||
|
for (var i = 0; i < Math.Min(5, Math.Min(result.Data.Bids.Length, result.Data.Asks.Length)); i++)
|
||||||
|
Console.WriteLine($"bid {result.Data.Bids[i].Price,15} | ask {result.Data.Asks[i].Price,15}");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task PrintBalancesAsync(string[] args)
|
||||||
|
{
|
||||||
|
var exchange = GetExchange(args, 1);
|
||||||
|
var result = await exchange.BalanceRest.GetBalancesAsync(new GetBalancesRequest(TradingMode.Spot));
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Balances request failed: {result.Error}");
|
||||||
|
Console.WriteLine("Set API credentials in environment variables before calling private endpoints.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var balance in result.Data.Where(x => x.Total != 0).OrderBy(x => x.Asset))
|
||||||
|
Console.WriteLine($"{balance.Asset,-8} available={balance.Available} total={balance.Total}");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task PrintOpenOrdersAsync(string[] args)
|
||||||
|
{
|
||||||
|
var exchange = GetExchange(args, 1);
|
||||||
|
var symbol = GetSymbol(args, 2);
|
||||||
|
|
||||||
|
var result = await exchange.OrderRest.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol));
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Open orders request failed: {result.Error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Data.Length == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("No open orders.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var order in result.Data)
|
||||||
|
Console.WriteLine($"{order.OrderId,-24} {order.Side,-4} {order.OrderType,-6} {order.Status,-12} {order.OrderQuantity?.QuantityInBaseAsset} @ {order.OrderPrice}");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task PlaceLimitOrderAsync(string[] args)
|
||||||
|
{
|
||||||
|
var exchange = GetExchange(args, 1);
|
||||||
|
var symbol = GetSymbol(args, 2);
|
||||||
|
var side = ParseSide(args.ElementAtOrDefault(4));
|
||||||
|
var quantity = ParseDecimal(args.ElementAtOrDefault(5), "quantity");
|
||||||
|
var price = ParseDecimal(args.ElementAtOrDefault(6), "price");
|
||||||
|
|
||||||
|
var request = new PlaceSpotOrderRequest(
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
SharedOrderType.Limit,
|
||||||
|
SharedQuantity.Base(quantity),
|
||||||
|
price,
|
||||||
|
SharedTimeInForce.GoodTillCanceled,
|
||||||
|
exchange.OrderRest.GenerateClientOrderId());
|
||||||
|
|
||||||
|
var validationError = exchange.OrderRest.PlaceSpotOrderOptions.ValidateRequest(
|
||||||
|
exchange.Name,
|
||||||
|
request,
|
||||||
|
TradingMode.Spot,
|
||||||
|
exchange.OrderRest.SupportedTradingModes,
|
||||||
|
exchange.OrderRest.SpotSupportedOrderTypes,
|
||||||
|
exchange.OrderRest.SpotSupportedTimeInForce,
|
||||||
|
exchange.OrderRest.SpotSupportedOrderQuantity);
|
||||||
|
if (validationError != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Order request is not valid for {exchange.Name}: {validationError}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await exchange.OrderRest.PlaceSpotOrderAsync(request);
|
||||||
|
Console.WriteLine(result.Success
|
||||||
|
? $"Order placed. Id: {result.Data.Id}"
|
||||||
|
: $"Order failed: {result.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task CancelOrderAsync(string[] args)
|
||||||
|
{
|
||||||
|
var exchange = GetExchange(args, 1);
|
||||||
|
var symbol = GetSymbol(args, 2);
|
||||||
|
var orderId = args.ElementAtOrDefault(4);
|
||||||
|
if (string.IsNullOrWhiteSpace(orderId))
|
||||||
|
throw new ArgumentException("Missing order id. Example: cancel-order binance BTC USDT 123456");
|
||||||
|
|
||||||
|
var result = await exchange.OrderRest.CancelSpotOrderAsync(new CancelOrderRequest(symbol, orderId));
|
||||||
|
Console.WriteLine(result.Success
|
||||||
|
? $"Order canceled. Id: {result.Data.Id}"
|
||||||
|
: $"Cancel failed: {result.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ExchangeClients GetExchange(string[] args, int index)
|
||||||
|
{
|
||||||
|
var name = args.ElementAtOrDefault(index);
|
||||||
|
if (name == null || !exchanges.TryGetValue(name, out var exchange))
|
||||||
|
throw new ArgumentException($"Unknown exchange `{name}`. Use one of: {string.Join(", ", exchanges.Keys)}");
|
||||||
|
|
||||||
|
return exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedSymbol GetSymbol(string[] args, int index)
|
||||||
|
{
|
||||||
|
var baseAsset = args.ElementAtOrDefault(index) ?? "BTC";
|
||||||
|
var quoteAsset = args.ElementAtOrDefault(index + 1) ?? "USDT";
|
||||||
|
return new SharedSymbol(TradingMode.Spot, baseAsset.ToUpperInvariant(), quoteAsset.ToUpperInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
static decimal ParseDecimal(string? value, string name)
|
||||||
|
{
|
||||||
|
if (!decimal.TryParse(value, out var result))
|
||||||
|
throw new ArgumentException($"Invalid {name} value `{value}`.");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SharedOrderSide ParseSide(string? value)
|
||||||
|
{
|
||||||
|
return value?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"buy" => SharedOrderSide.Buy,
|
||||||
|
"sell" => SharedOrderSide.Sell,
|
||||||
|
_ => throw new ArgumentException("Side should be `buy` or `sell`.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintHelp()
|
||||||
|
{
|
||||||
|
Console.WriteLine("CryptoExchange.Net SharedApis console example");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Public market data:");
|
||||||
|
Console.WriteLine(" prices [base] [quote] e.g. prices BTC USDT");
|
||||||
|
Console.WriteLine(" stream [exchange] [base] [quote] e.g. stream binance ETH USDT");
|
||||||
|
Console.WriteLine(" orderbook [exchange] [base] [quote] e.g. orderbook bybit BTC USDT");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Private endpoints, credentials required:");
|
||||||
|
Console.WriteLine(" balances [exchange]");
|
||||||
|
Console.WriteLine(" open-orders [exchange] [base] [quote]");
|
||||||
|
Console.WriteLine(" place-limit [exchange] [base] [quote] [buy|sell] [quantity] [price] --live");
|
||||||
|
Console.WriteLine(" cancel-order [exchange] [base] [quote] [order-id]");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Other:");
|
||||||
|
Console.WriteLine(" help");
|
||||||
|
Console.WriteLine(" exit");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal record ExchangeClients(
|
||||||
|
string Name,
|
||||||
|
ISpotTickerRestClient TickerRest,
|
||||||
|
IOrderBookRestClient OrderBookRest,
|
||||||
|
IBalanceRestClient BalanceRest,
|
||||||
|
ISpotOrderRestClient OrderRest,
|
||||||
|
ITickerSocketClient TickerSocket);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# CryptoExchange.Net Examples
|
||||||
|
|
||||||
|
| Path | Topic |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SharedClients` | Small runnable exchange agnostic quickstart for REST tickers and websocket ticker updates |
|
||||||
|
| `ConsoleClient` | Runnable CLI for some common operations |
|
||||||
|
| `ai-friendly` | Compact snippets that are easy to paste into small apps or coding-assistant prompts |
|
||||||
|
| `BlazorClient` | Server-side Blazor demo showing DI, live data, order books, and trackers |
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Binance.Net.Clients;
|
using Binance.Net.Clients;
|
||||||
using BitMart.Net.Clients;
|
using BitMart.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using OKX.Net.Clients;
|
using OKX.Net.Clients;
|
||||||
|
|
||||||
@@ -20,11 +21,15 @@ Console.WriteLine();
|
|||||||
Console.WriteLine("Press enter to start websocket");
|
Console.WriteLine("Press enter to start websocket");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
|
||||||
await SubscribeTickerUpdatesAsync(binanceSpotSocketClient, symbol);
|
var subscriptions = new List<UpdateSubscription>();
|
||||||
await SubscribeTickerUpdatesAsync(okxSpotSocketClient, symbol);
|
await SubscribeTickerUpdatesAsync(binanceSpotSocketClient, symbol, subscriptions);
|
||||||
await SubscribeTickerUpdatesAsync(bitmartSpotSocketClient, symbol);
|
await SubscribeTickerUpdatesAsync(okxSpotSocketClient, symbol, subscriptions);
|
||||||
|
await SubscribeTickerUpdatesAsync(bitmartSpotSocketClient, symbol, subscriptions);
|
||||||
|
|
||||||
|
Console.WriteLine("Press enter to stop websocket updates");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
|
foreach (var subscription in subscriptions)
|
||||||
|
await subscription.CloseAsync();
|
||||||
|
|
||||||
async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol symbol)
|
async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol symbol)
|
||||||
{
|
{
|
||||||
@@ -38,7 +43,7 @@ async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol sym
|
|||||||
Console.WriteLine($"{client.Exchange} {result.Data.Symbol}: {result.Data.LastPrice}");
|
Console.WriteLine($"{client.Exchange} {result.Data.Symbol}: {result.Data.LastPrice}");
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol)
|
async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol, ICollection<UpdateSubscription> subscriptions)
|
||||||
{
|
{
|
||||||
var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update =>
|
var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update =>
|
||||||
{
|
{
|
||||||
@@ -46,5 +51,10 @@ async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
|
{
|
||||||
Console.WriteLine($"Failed to subscribe ticker: {result.Error}");
|
Console.WriteLine($"Failed to subscribe ticker: {result.Error}");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.Add(result.Data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.11.0" />
|
<PackageReference Include="Binance.Net" Version="12.13.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.9.1" />
|
<PackageReference Include="BitMart.Net" Version="3.11.2" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.10.1" />
|
<PackageReference Include="JK.OKX.Net" Version="4.14.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
|||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 11.2.2 - 08 Jun 2026
|
||||||
|
* Fixed timing issue causing websocket connection to possible loop in error state
|
||||||
|
|
||||||
|
* Version 11.2.1 - 02 Jun 2026
|
||||||
|
* Fixed some testing logic
|
||||||
|
|
||||||
* Version 11.2.0 - 26 May 2026
|
* Version 11.2.0 - 26 May 2026
|
||||||
* Added RateLimitGroup client option to allow for specifying different rate limiting groups
|
* Added RateLimitGroup client option to allow for specifying different rate limiting groups
|
||||||
* Added request parameter checking in RestRequestValidator
|
* Added request parameter checking in RestRequestValidator
|
||||||
|
|||||||
Reference in New Issue
Block a user