1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 08:53:01 +00:00

Compare commits

..

48 Commits

Author SHA1 Message Date
Jkorf b215cccda4 Updated to version 9.6.0 2025-08-25 10:17:35 +02:00
Jkorf 3eda488361 Updated CryptoExchange.Net.Protobuf to CryptoExchange.Net version 9.5.0 2025-08-25 10:15:14 +02:00
Jkorf 993a44de35 Updated to version 9.6.0 2025-08-25 10:03:17 +02:00
Jkorf 99465f99a1 Fixed test 2025-08-25 10:00:42 +02:00
Jkorf d42de1fe90 Added support for parsing REST response even though status indicates error 2025-08-25 09:58:03 +02:00
Jkorf d0284c62c0 Removed obsolete attribute on Error.Code property, updated the description 2025-08-22 16:12:01 +02:00
Jkorf d92f3b7904 Added better support for subscriptions without subscribe confirmation 2025-08-22 10:17:16 +02:00
Jkorf 3e1b5ada69 Added check in websocket for receiving 401 unauthorized http response status when 101 was expected 2025-08-21 13:39:07 +02:00
Jkorf 6156fb8154 Fixed test 2025-08-19 14:42:03 +02:00
Jkorf f2753aed1e Updated to version 9.5.0 2025-08-19 10:21:24 +02:00
Jkorf e33d826381 Updated CryptoExchange.Net version to 9.5.0 2025-08-19 10:20:11 +02:00
Jkorf 364aa4d324 Updated to version 9.5.0 2025-08-19 10:07:07 +02:00
Jkorf 455b332757 Updated test methods 2025-08-19 10:05:14 +02:00
Jkorf 876b895645 Fixed warning 2025-08-19 10:01:49 +02:00
Jkorf daf7ed9fe6 Refactored RestApiClient authentication to prevent duplicate query string / body serialization 2025-08-19 09:50:10 +02:00
Jan Korf 3e365f83c9 Error handling update 2025-08-18 11:03:26 +02:00
Jkorf 40977ebdbe Fixed timing issue in query response processing 2025-08-07 14:15:18 +02:00
Jkorf 4a9058fc1c Fix response type in websocket queries not interested in the response 2025-08-07 14:15:00 +02:00
JKorf dab9a21608 Fixed IOrderBookSocketClient Shared interface not getting registered in DI 2025-08-04 17:12:34 +02:00
Jkorf a89c222399 Updated to version 9.4.0 2025-08-04 09:56:45 +02:00
Jkorf 1e356d2a45 Updated CryptoExchange.Net and protobuf-net package versions for CryptoExchange.Net.Protobuf 2025-08-04 09:54:15 +02:00
Jkorf eed794c2cf Updated to version 9.4.0 2025-08-04 09:47:36 +02:00
Jkorf 2f82e2015b Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported 2025-08-04 09:39:30 +02:00
Jkorf ad599badb2 Added CryptoExchange.Net tag 2025-07-29 16:02:38 +02:00
Jkorf 1e45c73f1d Added CoinW to examples 2025-07-29 15:57:02 +02:00
Jkorf 49c1fda2c1 Added CoinW reference 2025-07-29 11:44:22 +02:00
Jkorf 32a31e464b Updated to version 9.3.1 2025-07-29 09:42:53 +02:00
Jkorf cddb4167e4 Added Id property to SharedPosition model 2025-07-28 15:58:36 +02:00
Jkorf 65457d8df2 Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum 2025-07-28 15:58:19 +02:00
Jkorf 122a6cad43 Updated to version 9.3.0 2025-07-23 10:52:40 +02:00
Jkorf 4c0e841425 Updated CryptoExchange.Net version 2025-07-23 10:51:17 +02:00
Jkorf 92f5839aec Updated to version 9.3.0 2025-07-23 10:37:52 +02:00
Jan Korf 30475dae67 Feature/websocket listener update (#244)
Updated websocket message to listener matching logic to be more flexible
2025-07-23 10:31:03 +02:00
Jkorf 3d942bd503 Updated decimal parser to support "NaN" and "-Infinity" strings, added check for negative overflow value, improved performance in most cases 2025-07-21 09:42:52 +02:00
Jkorf f739520e52 Updated to version 9.2.1 2025-07-16 10:51:41 +02:00
Jkorf 0152603ddb Added setting for whether or not to process unparsable websocket messages 2025-07-16 10:42:38 +02:00
Jkorf aa06e0eead Fixed issue causing duplicate subscriptions and data in the TradeTracker and KlineTracker when websocket connection was reconnected 2025-07-16 09:27:42 +02:00
Jkorf 2fde9a285e Update README.md 2025-07-14 13:31:47 +02:00
Jkorf b9f6eb6abb Update README.md 2025-07-14 13:25:01 +02:00
Jkorf d77c4354a6 Updated to version 9.2.0 2025-07-14 11:42:40 +02:00
Jkorf 21860ddf85 Updated CryptoExchange.Net reference for CryptoExchange.Protobuf.Net 2025-07-14 11:33:30 +02:00
Jkorf 2cffa22cc2 Updated to version 9.2.0 2025-07-14 11:06:30 +02:00
EricGarnier 985ba9bb29 Simplify ArrayConverter (#241)
Co-authored-by: Eric GARNIER <ega@softfluent.com>
2025-07-14 10:57:48 +02:00
Jan Korf 96f23f163d Feature/protobuf (#243)
Protobuf implementation
2025-07-14 10:56:18 +02:00
Jkorf 0e7d49991a Updated examples, added Toobit reference 2025-06-11 14:33:05 +02:00
Jkorf 3e635cf0fe Updated to version 9.1.0 2025-05-28 15:03:32 +02:00
Jkorf 1425c66c69 Dependencies update
* Updated dotnet package versions from 9.0.0 to 9.0.5
* Replaced Microsoft.Extensions.Logging.Abstractions with icrosoft.Extensions.Logging
* Replaced Microsoft.Extensions.Options.ConfigurationExtensions with Microsoft.Extensions.Configuration.Binder, which includes a source generator for AOT publishing
* Removed redundant Microsoft.Extensions.DependencyInjection.Abstractions package reference
2025-05-28 14:50:23 +02:00
Jkorf fc3b7cc75b Added JsonConverter implementation for SharedQuantity and SharedSymbol types 2025-05-28 14:26:55 +02:00
93 changed files with 2724 additions and 676 deletions
@@ -0,0 +1,530 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using ProtoBuf;
using ProtoBuf.Meta;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
#if NET5_0_OR_GREATER
public abstract class ProtobufMessageAccessor<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
TIntermediateType> : IMessageAccessor
#else
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
#endif
{
/// <summary>
/// The intermediate deserialization object
/// </summary>
protected TIntermediateType? _intermediateType;
/// <summary>
/// Runtime type model
/// </summary>
protected RuntimeTypeModel _model;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => _intermediateType;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageAccessor(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach (var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
if (value == null)
return null;
var valueType = value.GetType();
if (valueType.IsArray)
return NodeType.Array;
if (IsSimple(valueType))
return NodeType.Value;
return NodeType.Object;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach(var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
return (T?)value;
}
/// <inheritdoc />
public T?[]? GetValues<T>(MessagePath path)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null);
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class ProtobufStreamMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
var result = _model.Deserialize(type, _stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null)
{
try
{
var result = _model.Deserialize<T>(_stream);
return new CallResult<T>(result);
}
catch(Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
IsValid = true;
return Task.FromResult(CallResult.SuccessResult);
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_intermediateType = default;
}
}
/// <summary>
/// Protobuf byte message accessor
/// </summary>
public class ProtobufByteMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
using var stream = new MemoryStream(_bytes.ToArray());
stream.Position = 0;
var result = _model.Deserialize(type, stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public override CallResult<T> Deserialize<
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(MessagePath? path = null)
#else
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
#endif
{
try
{
var result = _model.Deserialize<T>(_bytes);
return new CallResult<T>(result);
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
_intermediateType = _model.Deserialize<TIntermediate>(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_intermediateType = default;
}
}
}
@@ -0,0 +1,53 @@
using CryptoExchange.Net.Interfaces;
using ProtoBuf.Meta;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <inheritdoc />
public class ProtobufMessageSerializer : IByteMessageSerializer
{
private RuntimeTypeModel _model;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageSerializer(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public byte[] Serialize<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(T message)
#else
public byte[] Serialize<T>(T message)
#endif
{
using var memoryStream = new MemoryStream();
_model.Serialize(memoryStream, message);
return memoryStream.ToArray();
}
}
}
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.6.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
@@ -0,0 +1,128 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>CryptoExchange.Net.Protobuf</name>
</assembly>
<members>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
<summary>
System.Text.Json message accessor
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
<summary>
The intermediate deserialization object
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
<summary>
Runtime type model
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
<summary>
System.Text.Json stream message accessor
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
<summary>
Protobuf byte message accessor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
<inheritdoc />
</member>
</members>
</doc>
+22
View File
@@ -0,0 +1,22 @@
# ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Proto
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.Protobuf.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
Protobuf support for CryptoExchange.Net.
## Release notes
* 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 -14
View File
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
@@ -16,9 +17,9 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestBasicErrorCallResult()
{
var result = new CallResult(new ServerError("TestError"));
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.Message, "TestError");
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
@@ -36,9 +37,9 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestCallResultError()
{
var result = new CallResult<object>(new ServerError("TestError"));
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.Message, "TestError");
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
@@ -71,11 +72,11 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestCallResultErrorAs()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
@@ -84,11 +85,11 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestCallResultErrorAsError()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
@@ -97,11 +98,11 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestWebCallResultErrorAsError()
{
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
@@ -124,10 +125,10 @@ namespace CryptoExchange.Net.UnitTests
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
Assert.That(asResult.Error.Message == "TestError2");
Assert.That(asResult.Error.ErrorCode == "TestError2");
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
@@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.Code == 123);
Assert.That(result.Error.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
@@ -182,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test1", true)]
[TestCase("/sapi/test2", true)]
[TestCase("/api/test1", false)]
[TestCase("sapi/test1", false)]
[TestCase("sapi/test1", true)]
[TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{
@@ -7,6 +7,7 @@ using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests
{
@@ -223,13 +224,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("nan", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
}
[TestCase("1", 1)]
@@ -298,6 +303,40 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
public class STJDecimalObject
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
@@ -31,21 +32,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
internal class TestChannelQuery : Query<SubResponse>
{
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
}
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
{
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{
return new CallResult<SubResponse>(new ServerError(message.Data.Status));
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
}
return base.HandleMessage(connection, message);
return message.ToCallResult();
}
}
}
@@ -9,11 +9,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestQuery : Query<object>
{
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
ListenerIdentifiers = new HashSet<string> { identifier };
MessageMatcher = MessageMatcher.Create<object>(identifier);
}
}
}
@@ -15,22 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
private readonly Action<DataEvent<T>> _handler;
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
_handler = handler;
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
}
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
var data = (T)message.Data;
_handler.Invoke(message.As(data));
_handler.Invoke(message);
return new CallResult(null);
}
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
}
}
@@ -15,24 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
private readonly Action<DataEvent<T>> _handler;
private readonly string _channel;
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
{
ListenerIdentifiers = new HashSet<string>() { channel };
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
_handler = handler;
_channel = channel;
}
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
var data = (T)message.Data;
_handler.Invoke(message.As(data));
_handler.Invoke(message);
return new CallResult(null);
}
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
}
}
@@ -10,6 +10,7 @@ using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations;
@@ -55,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(data));
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>();
return deserializeResult;
@@ -77,10 +78,10 @@ namespace CryptoExchange.Net.UnitTests
{
}
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
}
public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret;
}
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Options;
using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization;
using CryptoExchange.Net.Objects.Errors;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -197,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
var errorData = accessor.Deserialize<TestError>();
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
}
public override TimeSpan? GetTimeOffset()
@@ -17,6 +17,7 @@ using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Net.WebSockets;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -98,7 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
protected internal override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
@@ -119,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public override string GetListenerIdentifier(IMessageAccessor message)
{
if (!message.IsJson)
if (!message.IsValid)
{
return "topic";
}
+6
View File
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,6 +43,10 @@ Global
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Authenticate a request. Output parameters should include the providedParameters input
/// Authenticate a request
/// </summary>
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="uri">The uri for the request</param>
/// <param name="method">The method of the request</param>
/// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="requestBodyFormat">The formatting of the request body</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
public abstract void AuthenticateRequest(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
ref IDictionary<string, object>? uriParameters,
ref IDictionary<string, object>? bodyParameters,
ref Dictionary<string, string>? headers,
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
RequestBodyFormat requestBodyFormat
);
/// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// SHA256 sign the data and return the bytes
@@ -465,10 +446,13 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
{
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return serializer.Serialize(value);
return stringSerializer.Serialize(value);
else
return serializer.Serialize(parameters);
return stringSerializer.Serialize(parameters);
}
}
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
@@ -54,6 +56,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// ctor
/// </summary>
@@ -87,6 +94,16 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
+59 -65
View File
@@ -11,6 +11,7 @@ using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
@@ -54,7 +55,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary>
/// Whether parameters need to be ordered
@@ -238,7 +239,7 @@ namespace CryptoExchange.Net.Clients
additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (result.Error is not CancellationRequestedError)
{
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
@@ -363,74 +364,58 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders)
{
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
var requestConfiguration = new RestRequestConfiguration(
definition,
baseAddress,
uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
new Dictionary<string, string>(additionalHeaders ?? []),
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
var uri = new Uri(baseAddress.AppendPath(definition.Path));
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
Dictionary<string, string>? headers = null;
if (AuthenticationProvider != null)
try
{
try
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
definition.Method,
ref uriParams,
ref bodyParams,
ref headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat
);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
var queryString = requestConfiguration.GetQueryString(true);
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
queryString = $"?{queryString}";
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
if (uriParams != null)
uri = uri.SetParameters(uriParams, arraySerialization);
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
if (headers != null)
{
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value);
if (additionalHeaders != null)
foreach (var header in StandardRequestHeaders)
{
foreach (var header in additionalHeaders)
// Only add it if it isn't overwritten
if (!requestConfiguration.Headers.ContainsKey(header.Key))
request.AddHeader(header.Key, header.Value);
}
}
if (StandardRequestHeaders != null)
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
{
foreach (var header in StandardRequestHeaders)
var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
var bodyContent = requestConfiguration.GetBodyContent();
if (bodyContent != null)
{
// Only add it if it isn't overwritten
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
request.SetContent(bodyContent, contentType);
}
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParams != null && bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
{
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
}
return request;
@@ -439,11 +424,13 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
/// <param name="requestDefinition">The request definition</param>
/// <param name="request">The request object to execute</param>
/// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
RequestDefinition requestDefinition,
IRequest request,
IRateLimitGate? gate,
CancellationToken cancellationToken)
@@ -463,7 +450,7 @@ namespace CryptoExchange.Net.Clients
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
accessor = CreateAccessor();
if (!response.IsSuccessStatusCode)
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{
// Error response
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
@@ -499,12 +486,11 @@ namespace CryptoExchange.Net.Clients
if (!valid)
{
// Invalid json
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
}
// Json response received
var parsedError = TryParseError(response.ResponseHeaders, accessor);
var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
@@ -526,7 +512,8 @@ namespace CryptoExchange.Net.Clients
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException));
var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
catch (OperationCanceledException canceledException)
{
@@ -538,7 +525,9 @@ namespace CryptoExchange.Net.Clients
else
{
// Request timed out
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException));
var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
}
finally
@@ -554,10 +543,11 @@ namespace CryptoExchange.Net.Clients
/// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null
/// </summary>
/// <param name="requestDefinition">Request definition</param>
/// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@@ -603,12 +593,16 @@ namespace CryptoExchange.Net.Clients
{
if (contentType == Constants.JsonContentHeader)
{
var serializer = CreateSerializer();
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
// Write the parameters as json in the body
string stringData;
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(value);
stringData = stringSerializer.Serialize(value);
else
stringData = CreateSerializer().Serialize(parameters);
stringData = stringSerializer.Serialize(parameters);
request.SetContent(stringData, contentType);
}
else if (contentType == Constants.FormContentHeader)
@@ -629,7 +623,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{
return new ServerError(null, "Unknown request error", exception);
return new ServerError(ErrorInfo.Unknown, exception);
}
/// <summary>
+13 -9
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting;
@@ -82,6 +83,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary>
/// Whether to continue processing and forward unparsable messages to handlers
/// </summary>
protected internal bool ProcessUnparsableMessages { get; set; } = false;
/// <inheritdoc />
public double IncomingKbps
{
@@ -138,7 +144,7 @@ namespace CryptoExchange.Net.Clients
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal abstract IByteMessageAccessor CreateAccessor();
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
/// <summary>
/// Create a serializer instance
@@ -260,11 +266,11 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection);
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null)
{
// Send the request and wait for answer
@@ -308,11 +314,10 @@ namespace CryptoExchange.Net.Clients
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary>
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
{
return QueryAsync(BaseAddress, query, ct);
}
@@ -321,12 +326,11 @@ namespace CryptoExchange.Net.Clients
/// Send a query on a socket connection and wait for the response
/// </summary>
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="url">The url for the request</param>
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
{
if (_disposing)
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
@@ -365,7 +369,7 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
if (ct.IsCancellationRequested)
@@ -811,7 +815,7 @@ namespace CryptoExchange.Net.Clients
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
});
}
});
@@ -25,8 +25,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
@@ -100,17 +98,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = Activator.CreateInstance(typeof(T))!;
return (T)ParseObject(ref reader, result, typeof(T), options);
var result = new T();
return ParseObject(ref reader, result, options);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options)
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#else
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
@@ -135,20 +133,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
object? value = null;
if (attribute.JsonConverter != null)
{
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
{
newOptions = new JsonSerializerOptions
if (attribute.JsonSerializerOptions == null)
{
attribute.JsonSerializerOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
_converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
}
else if (attribute.DefaultDeserialization)
{
@@ -231,6 +228,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
}
}
}
@@ -19,22 +19,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try
{
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
return ExchangeHelpers.ParseDecimal(value);
}
try
@@ -0,0 +1,60 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
var result = new T();
result.QuantityInBaseAsset = baseQuantity;
result.QuantityInQuoteAsset = quoteQuantity;
result.QuantityInContracts = contractQuantity;
return result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
if (value.QuantityInBaseAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
if (value.QuantityInQuoteAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
if (value.QuantityInContracts == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInContracts.Value);
writer.WriteEndArray();
}
}
}
@@ -0,0 +1,46 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
{
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
reader.Read();
var baseAsset = reader.GetString()!;
reader.Read();
var quoteAsset = reader.GetString()!;
reader.Read();
var timeStr = reader.GetString()!;
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
}
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteStringValue(value.TradingMode.ToString());
writer.WriteStringValue(value.BaseAsset);
writer.WriteStringValue(value.QuoteAsset);
writer.WriteStringValue(value.DeliverTime?.ToString());
writer.WriteEndArray();
}
}
}
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsJson { get; set; }
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
if (!IsValid)
return new CallResult<object>(GetOriginalString());
if (_document == null)
@@ -60,13 +60,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
catch (JsonException ex)
{
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
var info = $"Deserialize unknown Exception: {ex.Message}";
return new CallResult<object>(new DeserializeError(info, ex));
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
@@ -87,20 +86,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
catch (JsonException ex)
{
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
var info = $"Unknown exception: {ex.Message}";
return new CallResult<T>(new DeserializeError(info, ex));
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
@@ -117,7 +115,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
@@ -139,7 +137,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
public T? GetValue<T>(MessagePath path)
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
@@ -173,7 +171,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
@@ -188,7 +186,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
@@ -279,14 +277,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsJson = true;
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
@@ -337,19 +335,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsJson = false;
return new CallResult(new ServerError("Not a json value"));
IsValid = false;
return new CallResult(new DeserializeError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsJson = true;
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
@@ -7,7 +7,7 @@ using System.Text.Json.Serialization.Metadata;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <inheritdoc />
public class SystemTextJsonMessageSerializer : IMessageSerializer
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
{
private readonly JsonSerializerOptions _options;
+10 -15
View File
@@ -6,11 +6,11 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.0.1</PackageVersion>
<AssemblyVersion>9.0.1</AssemblyVersion>
<FileVersion>9.0.1</FileVersion>
<PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.6.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
@@ -37,12 +37,6 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup>
@@ -57,10 +51,11 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="System.Text.Json" Version="9.0.6" />
</ItemGroup>
<ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
</ItemGroup>
</Project>
+44
View File
@@ -2,6 +2,7 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
@@ -341,5 +342,48 @@ namespace CryptoExchange.Net
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
}
/// <summary>
/// Parse a decimal value from a string
/// </summary>
public static decimal? ParseDecimal(string? value)
{
// Value is null or empty is the most common case to return null so check before trying to parse
if (string.IsNullOrEmpty(value))
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
return decValue;
// Check for values which should be parsed to null
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Infinity value should be parsed to min/max value
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
// assume overflow
if (overflowValue < 0)
return decimal.MinValue;
else
return decimal.MaxValue;
}
// Unknown decimal format, return null
return null;
}
}
}
+2 -2
View File
@@ -23,14 +23,14 @@ namespace CryptoExchange.Net
{
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
{
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
_symbolInfos.TryAdd(topicId, exchangeInfo);
}
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
}
/// <summary>
+2 -2
View File
@@ -498,8 +498,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineSocketClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickerSocketClient)client(x)!);
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
@@ -2,6 +2,7 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
@@ -13,9 +14,9 @@ namespace CryptoExchange.Net.Interfaces
public interface IMessageAccessor
{
/// <summary>
/// Is this a json message
/// Is this a valid message
/// </summary>
bool IsJson { get; }
bool IsValid { get; }
/// <summary>
/// Is the original data available for retrieval
/// </summary>
@@ -59,12 +60,20 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<object> Deserialize(Type type, MessagePath? path = null);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<T> Deserialize<T>(MessagePath? path = null);
/// <summary>
@@ -17,22 +17,13 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public int Id { get; }
/// <summary>
/// The identifiers for this processor
/// The matcher for this listener
/// </summary>
public HashSet<string> ListenerIdentifiers { get; }
public MessageMatcher MessageMatcher { get; }
/// <summary>
/// Handle a message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
Type? GetMessageType(IMessageAccessor messageAccessor);
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matchedHandler);
/// <summary>
/// Deserialize a message into object of type
/// </summary>
@@ -1,9 +1,31 @@
namespace CryptoExchange.Net.Interfaces
using System.Diagnostics.CodeAnalysis;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Serializer interface
/// </summary>
public interface IMessageSerializer
{
}
/// <summary>
/// Serialize to byte array
/// </summary>
public interface IByteMessageSerializer: IMessageSerializer
{
/// <summary>
/// Serialize an object to a string
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
byte[] Serialize<T>(T message);
}
/// <summary>
/// Serialize to string
/// </summary>
public interface IStringMessageSerializer: IMessageSerializer
{
/// <summary>
/// Serialize an object to a string
+8 -1
View File
@@ -78,13 +78,20 @@ namespace CryptoExchange.Net.Interfaces
/// <returns></returns>
Task<CallResult> ConnectAsync(CancellationToken ct);
/// <summary>
/// Send data
/// Send string data
/// </summary>
/// <param name="id"></param>
/// <param name="data"></param>
/// <param name="weight"></param>
bool Send(int id, string data, int weight);
/// <summary>
/// Send byte data
/// </summary>
/// <param name="id"></param>
/// <param name="data"></param>
/// <param name="weight"></param>
bool Send(int id, byte[] data, int weight);
/// <summary>
/// Reconnect the socket
/// </summary>
/// <returns></returns>
@@ -15,9 +15,10 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
private static readonly Action<ILogger, int, string, Exception?> _failedToParse;
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
private static readonly Action<ILogger, int, int, string, Exception?> _processorMatched;
private static readonly Action<ILogger, int, string, string, Exception?> _processorMatched;
private static readonly Action<ILogger, int, int, Exception?> _receivedMessageNotRecognized;
private static readonly Action<ILogger, int, string?, Exception?> _failedToDeserializeMessage;
private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed;
@@ -37,6 +38,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
static SocketConnectionLoggingExtension()
{
@@ -90,11 +92,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2009, "ErrorProcessingMessage"),
"[Sckt {SocketId}] error processing message");
_processorMatched = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2010, "ProcessorMatched"),
"[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}");
_receivedMessageNotRecognized = LoggerMessage.Define<int, int>(
LogLevel.Warning,
new EventId(2011, "ReceivedMessageNotRecognized"),
@@ -188,7 +185,23 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}");
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(2030, "FailedToParse"),
"[Sckt {SocketId}] failed to parse data: {Error}");
_sendingByteData = LoggerMessage.Define<int, int, int>(
LogLevel.Trace,
new EventId(2031, "SendingByteData"),
"[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}");
_processorMatched = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(2032, "ProcessorMatched"),
"[Sckt {SocketId}] listener '{ListenId}' matched to message with listener identifier {ListenerId}");
}
public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
@@ -230,6 +243,12 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_receivedData(logger, socketId, originalData, null);
}
public static void FailedToParse(this ILogger logger, int socketId, string error)
{
_failedToParse(logger, socketId, error, null);
}
public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData)
{
_failedToEvaluateMessage(logger, socketId, originalData, null);
@@ -238,9 +257,9 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_errorProcessingMessage(logger, socketId, e);
}
public static void ProcessorMatched(this ILogger logger, int socketId, int count, string listenerId)
public static void ProcessorMatched(this ILogger logger, int socketId, string listener, string listenerId)
{
_processorMatched(logger, socketId, count, listenerId, null);
_processorMatched(logger, socketId, listener, listenerId, null);
}
public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id)
{
@@ -321,5 +340,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
{
_sendingByteData(logger, socketId, requestId, length, null);
}
}
}
+16
View File
@@ -251,4 +251,20 @@ namespace CryptoExchange.Net.Objects
/// </summary>
DEX
}
/// <summary>
/// Timeout behavior for queries
/// </summary>
public enum TimeoutBehavior
{
/// <summary>
/// Fail the request
/// </summary>
Fail,
/// <summary>
/// Mark the query as successful
/// </summary>
Succeed
}
}
+148 -57
View File
@@ -1,4 +1,5 @@
using System;
using CryptoExchange.Net.Objects.Errors;
using System;
namespace CryptoExchange.Net.Objects
{
@@ -7,15 +8,52 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public abstract class Error
{
private int? _code;
/// <summary>
/// The error code from the server
/// The int error code the server returned; or the http status code int value if there was no error code.<br />
/// <br />
/// <i>Note:</i><br />
/// The <see cref="ErrorCode"/> property should be used for more generic error checking; it might contain a string error code if the server does not return an int code.
/// </summary>
public int? Code { get; set; }
public int? Code
{
get
{
if (_code.HasValue)
return _code;
return int.TryParse(ErrorCode, out var r) ? r : null;
}
set
{
_code = value;
}
}
/// <summary>
/// The message for the error that occurred
/// The error code returned by the server
/// </summary>
public string Message { get; set; }
public string? ErrorCode { get; set; }
/// <summary>
/// The error description
/// </summary>
public string? ErrorDescription { get; set; }
/// <summary>
/// Error type
/// </summary>
public ErrorType ErrorType { get; set; }
/// <summary>
/// Whether the error is transient and can be retried
/// </summary>
public bool IsTransient { get; set; }
/// <summary>
/// The server message for the error that occurred
/// </summary>
public string? Message { get; set; }
/// <summary>
/// Underlying exception
@@ -25,10 +63,13 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
protected Error (int? code, string message, Exception? exception)
protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception)
{
Code = code;
Message = message;
ErrorCode = errorCode;
ErrorType = errorInfo.ErrorType;
Message = errorInfo.Message;
ErrorDescription = errorInfo.ErrorDescription;
IsTransient = errorInfo.IsTransient;
Exception = exception;
}
@@ -38,7 +79,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public override string ToString()
{
return Code != null ? $"[{GetType().Name}] {Code}: {Message}" : $"[{GetType().Name}] {Message}";
return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}";
}
}
@@ -48,19 +89,24 @@ namespace CryptoExchange.Net.Objects
public class CantConnectError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public CantConnectError() : base(null, "Can't connect to the server", null) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.UnableToConnect, false, "Can't connect to the server");
/// <summary>
/// ctor
/// </summary>
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { }
public CantConnectError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { }
public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -69,14 +115,19 @@ namespace CryptoExchange.Net.Objects
public class NoApiCredentialsError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint");
/// <summary>
/// ctor
/// </summary>
protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { }
public NoApiCredentialsError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -87,12 +138,19 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
public ServerError(string message) : base(null, message, null) { }
public ServerError(ErrorInfo errorInfo, Exception? exception = null)
: base(null, errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
public ServerError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null)
: this(errorCode.ToString(), errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { }
}
/// <summary>
@@ -101,14 +159,30 @@ namespace CryptoExchange.Net.Objects
public class WebError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public WebError(string message, Exception? exception = null) : base(null, message, exception) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.NetworkError, true, "Failed to complete the request to the server due to a network error");
/// <summary>
/// ctor
/// </summary>
public WebError(int code, string message, Exception? exception = null) : base(code, message, exception) { }
public WebError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
/// Timeout error waiting for a response from the server
/// </summary>
public class TimeoutError : Error
{
/// <summary>
/// Default error info
/// </summary>
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time");
/// <summary>
/// ctor
/// </summary>
public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
@@ -117,30 +191,14 @@ namespace CryptoExchange.Net.Objects
public class DeserializeError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public DeserializeError(string message, Exception? exception = null) : base(null, message, exception) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data");
/// <summary>
/// ctor
/// </summary>
protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
}
/// <summary>
/// Unknown error
/// </summary>
public class UnknownError : Error
{
/// <summary>
/// ctor
/// </summary>
public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { }
/// <summary>
/// ctor
/// </summary>
protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { }
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
@@ -149,14 +207,28 @@ namespace CryptoExchange.Net.Objects
public class ArgumentError : Error
{
/// <summary>
/// ctor
/// Default error info for missing parameter
/// </summary>
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
protected static readonly ErrorInfo _missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter");
/// <summary>
/// Default error info for invalid parameter
/// </summary>
protected static readonly ErrorInfo _invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter");
/// <summary>
/// ctor
/// </summary>
protected ArgumentError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
public static ArgumentError Missing(string parameterName, string? message = null) => new ArgumentError(_missingInfo with { Message = message == null ? $"{_missingInfo.Message} '{parameterName}'" : $"{_missingInfo.Message} '{parameterName}': {message}" }, null);
/// <summary>
/// ctor
/// </summary>
public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(_invalidInfo with { Message = $"{_invalidInfo.Message} '{parameterName}': {message}" }, null);
/// <summary>
/// ctor
/// </summary>
protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -172,7 +244,7 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { }
protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
}
/// <summary>
@@ -181,15 +253,19 @@ namespace CryptoExchange.Net.Objects
public class ClientRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
/// <param name="message"></param>
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded");
/// <summary>
/// ctor
/// </summary>
protected ClientRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
public ClientRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
/// <summary>
/// ctor
/// </summary>
protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
}
/// <summary>
@@ -198,14 +274,19 @@ namespace CryptoExchange.Net.Objects
public class ServerRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public ServerRateLimitError(string? message = null, Exception? exception = null) : base(null, "Server rate limit exceeded" + (message?.Length > 0 ? " : " + message : null), exception) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded");
/// <summary>
/// ctor
/// </summary>
protected ServerRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
public ServerRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
/// <summary>
/// ctor
/// </summary>
protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
}
/// <summary>
@@ -214,14 +295,19 @@ namespace CryptoExchange.Net.Objects
public class CancellationRequestedError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
/// <summary>
/// ctor
/// </summary>
public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -230,13 +316,18 @@ namespace CryptoExchange.Net.Objects
public class InvalidOperationError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid");
/// <summary>
/// ctor
/// </summary>
protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
public InvalidOperationError(string message) : base(null, _errorInfo with { Message = message }, null) { }
/// <summary>
/// ctor
/// </summary>
protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error evaluator
/// </summary>
public class ErrorEvaluator
{
/// <summary>
/// Error code
/// </summary>
public string[] ErrorCodes { get; set; }
/// <summary>
/// Evaluation callback for determining the error type
/// </summary>
public Func<string, string?, ErrorInfo> ErrorTypeEvaluator { get; set; }
/// <summary>
/// ctor
/// </summary>
public ErrorEvaluator(string errorCode, Func<string, string?, ErrorInfo> errorTypeEvaluator)
{
ErrorCodes = [errorCode];
ErrorTypeEvaluator = errorTypeEvaluator;
}
/// <summary>
/// ctor
/// </summary>
public ErrorEvaluator(string[] errorCodes, Func<string, string?, ErrorInfo> errorTypeEvaluator)
{
ErrorCodes = errorCodes;
ErrorTypeEvaluator = errorTypeEvaluator;
}
}
}
@@ -0,0 +1,58 @@
using System;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error info
/// </summary>
public record ErrorInfo
{
/// <summary>
/// Unknown error info
/// </summary>
public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []);
/// <summary>
/// The server error code
/// </summary>
public string[] ErrorCodes { get; set; }
/// <summary>
/// Error description
/// </summary>
public string? ErrorDescription { get; set; }
/// <summary>
/// The error type
/// </summary>
public ErrorType ErrorType { get; set; }
/// <summary>
/// Whether the error is transient and can be retried
/// </summary>
public bool IsTransient { get; set; }
/// <summary>
/// Server response message
/// </summary>
public string? Message { get; set; }
/// <summary>
/// ctor
/// </summary>
public ErrorInfo(ErrorType errorType, string description)
{
ErrorCodes = [];
ErrorType = errorType;
IsTransient = false;
ErrorDescription = description;
}
/// <summary>
/// ctor
/// </summary>
public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes)
{
ErrorCodes = errorCodes;
ErrorType = errorType;
IsTransient = isTransient;
ErrorDescription = description;
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error mapping collection
/// </summary>
public class ErrorMapping
{
private Dictionary<string, ErrorEvaluator> _evaluators = new Dictionary<string, ErrorEvaluator>();
private Dictionary<string, ErrorInfo> _directMapping = new Dictionary<string, ErrorInfo>();
/// <summary>
/// ctor
/// </summary>
public ErrorMapping(ErrorInfo[] errorMappings, ErrorEvaluator[]? errorTypeEvaluators = null)
{
foreach (var item in errorMappings)
{
if (!item.ErrorCodes.Any())
throw new Exception("Error codes can't be null in error mapping");
foreach(var code in item.ErrorCodes!)
_directMapping.Add(code, item);
}
if (errorTypeEvaluators == null)
return;
foreach (var item in errorTypeEvaluators)
{
foreach(var code in item.ErrorCodes)
_evaluators.Add(code, item);
}
}
/// <summary>
/// Get error info for an error code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message)
{
if (_directMapping.TryGetValue(code!, out var info))
return info with { Message = message };
if (_evaluators.TryGetValue(code!, out var eva))
return eva.ErrorTypeEvaluator.Invoke(code!, message) with { Message = message };
return ErrorInfo.Unknown with { Message = message };
}
}
}
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Type of error
/// </summary>
public enum ErrorType
{
#region Library errors
/// <summary>
/// Failed to connect to server
/// </summary>
UnableToConnect,
/// <summary>
/// Failed to complete the request to the server
/// </summary>
NetworkError,
/// <summary>
/// No API credentials have been specified
/// </summary>
MissingCredentials,
/// <summary>
/// Invalid parameter value
/// </summary>
InvalidParameter,
/// <summary>
/// Missing parameter value
/// </summary>
MissingParameter,
/// <summary>
/// Cancellation requested by user
/// </summary>
CancellationRequested,
/// <summary>
/// Invalid operation requested
/// </summary>
InvalidOperation,
/// <summary>
/// Failed to deserialize data
/// </summary>
DeserializationFailed,
/// <summary>
/// Websocket is temporarily paused
/// </summary>
WebsocketPaused,
/// <summary>
/// Timeout while waiting for data from the order book subscription
/// </summary>
OrderBookTimeout,
/// <summary>
/// All orders failed for a multi-order operation
/// </summary>
AllOrdersFailed,
/// <summary>
/// Request timeout
/// </summary>
Timeout,
#endregion
#region Server errors
/// <summary>
/// Unknown error
/// </summary>
Unknown,
/// <summary>
/// Not authorized or insufficient permissions
/// </summary>
Unauthorized,
/// <summary>
/// Request rate limit error, too many requests
/// </summary>
RateLimitRequest,
/// <summary>
/// Connection rate limit error, too many connections
/// </summary>
RateLimitConnection,
/// <summary>
/// Subscription rate limit error, too many subscriptions
/// </summary>
RateLimitSubscription,
/// <summary>
/// Order rate limit error, too many orders
/// </summary>
RateLimitOrder,
/// <summary>
/// Request timestamp invalid
/// </summary>
InvalidTimestamp,
/// <summary>
/// Unknown symbol
/// </summary>
UnknownSymbol,
/// <summary>
/// Unknown asset
/// </summary>
UnknownAsset,
/// <summary>
/// Unknown order
/// </summary>
UnknownOrder,
/// <summary>
/// Duplicate subscription
/// </summary>
DuplicateSubscription,
/// <summary>
/// Invalid quantity
/// </summary>
InvalidQuantity,
/// <summary>
/// Invalid price
/// </summary>
InvalidPrice,
/// <summary>
/// Parameter(s) for stop or tp/sl order invalid
/// </summary>
InvalidStopParameters,
/// <summary>
/// Not enough balance to execute request
/// </summary>
InsufficientBalance,
/// <summary>
/// Client order id already in use
/// </summary>
DuplicateClientOrderId,
/// <summary>
/// Symbol is not currently trading
/// </summary>
UnavailableSymbol,
/// <summary>
/// Order rejected due to order configuration such as order type or time in force restrictions
/// </summary>
RejectedOrderConfiguration,
/// <summary>
/// There is no open position
/// </summary>
NoPosition,
/// <summary>
/// Max position reached
/// </summary>
MaxPosition,
/// <summary>
/// Error in the internal system
/// </summary>
SystemError,
/// <summary>
/// The target object is not in the correct state for an operation
/// </summary>
IncorrectState,
/// <summary>
/// Risk management error
/// </summary>
RiskError
#endregion
}
}
@@ -62,6 +62,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public bool PreventCaching { get; set; }
/// <summary>
/// Whether the response to this requests should attempted to be parsed even when the status indicates failure
/// </summary>
public bool TryParseOnNonSuccess { get; set; }
/// <summary>
/// Connection id
/// </summary>
@@ -76,6 +81,9 @@ namespace CryptoExchange.Net.Objects
{
Path = path;
Method = method;
if (!Path.StartsWith("/"))
Path = $"/{Path}";
}
/// <inheritdoc />
@@ -46,6 +46,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
HttpMethod method,
@@ -57,8 +58,9 @@ namespace CryptoExchange.Net.Objects
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
bool? preventCaching = null,
bool? tryParseOnNonSuccess = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
@@ -86,7 +89,8 @@ namespace CryptoExchange.Net.Objects
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
bool? preventCaching = null,
bool? tryParseOnNonSuccess = null)
{
if (!_definitions.TryGetValue(identifier, out var def))
@@ -100,7 +104,8 @@ namespace CryptoExchange.Net.Objects
ArraySerialization = arraySerialization,
RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false
PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
};
_definitions.TryAdd(identifier, def);
}
@@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Rest request configuration
/// </summary>
public class RestRequestConfiguration
{
private string? _bodyContent;
private string? _queryString;
/// <summary>
/// Http method
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Whether the request needs authentication
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Base address for the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The request path
/// </summary>
public string Path { get; set; }
/// <summary>
/// Query parameters
/// </summary>
public IDictionary<string, object> QueryParameters { get; set; }
/// <summary>
/// Body parameters
/// </summary>
public IDictionary<string, object> BodyParameters { get; set; }
/// <summary>
/// Request headers
/// </summary>
public IDictionary<string, string> Headers { get; set; }
/// <summary>
/// Array serialization type
/// </summary>
public ArrayParametersSerialization ArraySerialization { get; set; }
/// <summary>
/// Position of the parameters
/// </summary>
public HttpMethodParameterPosition ParameterPosition { get; set; }
/// <summary>
/// Body format
/// </summary>
public RequestBodyFormat BodyFormat { get; set; }
/// <summary>
/// ctor
/// </summary>
public RestRequestConfiguration(
RequestDefinition requestDefinition,
string baseAddress,
IDictionary<string, object> queryParams,
IDictionary<string, object> bodyParams,
IDictionary<string, string> headers,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parametersPosition,
RequestBodyFormat bodyFormat)
{
Method = requestDefinition.Method;
Authenticated = requestDefinition.Authenticated;
Path = requestDefinition.Path;
BaseAddress = baseAddress;
QueryParameters = queryParams;
BodyParameters = bodyParams;
Headers = headers;
ArraySerialization = arraySerialization;
ParameterPosition = parametersPosition;
BodyFormat = bodyFormat;
}
/// <summary>
/// Get the parameter collection based on the ParameterPosition
/// </summary>
public IDictionary<string, object> GetPositionParameters()
{
if (ParameterPosition == HttpMethodParameterPosition.InBody)
return BodyParameters;
return QueryParameters;
}
/// <summary>
/// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that.
/// </summary>
/// <param name="urlEncode">Whether to URL encode the parameter string if creating new</param>
public string GetQueryString(bool urlEncode = true)
{
return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization);
}
/// <summary>
/// Set the query string of the request. Will be returned by subsequent <see cref="GetQueryString" /> calls
/// </summary>
public void SetQueryString(string value)
{
_queryString = value;
}
/// <summary>
/// Get the body content if it's previously set
/// </summary>
public string? GetBodyContent()
{
return _bodyContent;
}
/// <summary>
/// Set the body content for the request
/// </summary>
public void SetBodyContent(string content)
{
_bodyContent = content;
}
}
}
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
@@ -549,7 +550,7 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > timeout)
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
@@ -20,6 +20,10 @@
/// <summary>
/// Quantity can be either base or quote quantity
/// </summary>
BaseAndQuoteAsset
BaseAndQuoteAsset,
/// <summary>
/// Quantity can be either base or quote quantity, or in contracts
/// </summary>
BaseAndQuoteAssetAndContracts
}
}
@@ -58,19 +58,19 @@ namespace CryptoExchange.Net.SharedApis
public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
{
if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value))
return new ArgumentError($"ApiType.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
foreach (var param in RequiredExchangeParameters)
{
if (!string.IsNullOrEmpty(param.Name))
{
if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true)
return new ArgumentError($"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}");
return ArgumentError.Invalid(param.Name!, $"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}");
}
else
{
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
}
}
@@ -109,6 +109,15 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public List<ParameterDescription> RequiredOptionalParameters { get; set; } = new List<ParameterDescription>();
/// <summary>
/// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters)
/// </summary>
public bool SupportsMultipleSymbols { get; set; } = false;
/// <summary>
/// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters)
/// </summary>
public int? MaxSymbolCount { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -131,12 +140,25 @@ namespace CryptoExchange.Net.SharedApis
if (!string.IsNullOrEmpty(param.Name))
{
if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
return ArgumentError.Invalid(param.Name!, $"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
}
else
{
if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
}
}
if (request is SharedSymbolRequest symbolsRequest)
{
if (symbolsRequest.Symbols != null)
{
if (!SupportsMultipleSymbols)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported");
if (symbolsRequest.Symbols.Length > MaxSymbolCount)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed");
}
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (TimeFilterSupported && request.StartTime != null)
return new ArgumentError($"Time filter is not supported");
return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (TimeFilterSupported && request.StartTime != null)
return new ArgumentError($"Time filter is not supported");
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -67,23 +67,23 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (!IsSupported(request.Interval))
return new ArgumentError("Interval not supported");
return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported");
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} klines are available");
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
if (request.Limit > MaxLimit)
return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request");
return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request");
if (MaxTotalDataPoints.HasValue)
{
if (request.Limit > MaxTotalDataPoints.Value)
return new ArgumentError($"Only the most recent {MaxTotalDataPoints} klines are available");
return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available");
if (request.StartTime.HasValue == true)
{
if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value)
return new ArgumentError($"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed");
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed");
}
}
@@ -49,13 +49,13 @@ namespace CryptoExchange.Net.SharedApis
return null;
if (MaxLimit.HasValue && request.Limit.Value > MaxLimit)
return new ArgumentError($"Max limit is {MaxLimit}");
return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}");
if (MinLimit.HasValue && request.Limit.Value < MinLimit)
return new ArgumentError($"Min limit is {MaxLimit}");
return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MaxLimit}");
if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value))
return new ArgumentError($"Limit should be one of " + string.Join(", ", SupportedLimits));
return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Limit should be one of " + string.Join(", ", SupportedLimits));
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public Error? Validate(GetRecentTradesRequest request)
{
if (request.Limit > MaxLimit)
return new ArgumentError($"Only the most recent {MaxLimit} trades are available");
return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available");
return null;
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} trades are available");
return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (TimeFilterSupported && request.StartTime != null)
return new ArgumentError($"Time filter is not supported");
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -36,16 +36,16 @@ namespace CryptoExchange.Net.SharedApis
SharedQuantitySupport quantitySupport)
{
if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null))
return new ArgumentError("Tp/Sl parameters not supported");
return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.StopLossPrice) + " / " + nameof(PlaceFuturesOrderRequest.TakeProfitPrice), "Tp/Sl parameters not supported");
if (request.OrderType == SharedOrderType.Other)
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
if (!supportedOrderTypes.Contains(request.OrderType))
return new ArgumentError("Order type not supported");
return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.OrderType), "Order type not supported");
if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value))
return new ArgumentError("Order time in force not supported");
return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.TimeInForce), "Order time in force not supported");
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
if (quantityError != null)
@@ -34,10 +34,10 @@ namespace CryptoExchange.Net.SharedApis
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
if (!supportedOrderTypes.Contains(request.OrderType))
return new ArgumentError("Order type not supported");
return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.OrderType), "Order type not supported");
if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value))
return new ArgumentError("Order time in force not supported");
return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.TimeInForce), "Order time in force not supported");
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
if (quantityError != null)
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (!IsSupported(request.Interval))
return new ArgumentError("Interval not supported");
return ArgumentError.Invalid(nameof(SubscribeKlineRequest.Interval), "Interval not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{
if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value))
return new ArgumentError("Limit not supported");
return ArgumentError.Invalid(nameof(SubscribeOrderBookRequest.Limit), "Limit not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
}
@@ -65,6 +65,9 @@ namespace CryptoExchange.Net.SharedApis
if (supportedType == quantityType)
return true;
if (supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
return true;
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset))
return true;
@@ -77,20 +80,20 @@ namespace CryptoExchange.Net.SharedApis
public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity)
{
var supportedType = GetSupportedQuantityType(side, type);
if (supportedType == SharedQuantityType.BaseAndQuoteAsset)
if (supportedType == SharedQuantityType.BaseAndQuoteAsset || supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
return null;
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null)
return new ArgumentError($"Quantity for {side}.{type} required in base or quote asset");
return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base or quote asset");
if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null)
return new ArgumentError($"Quantity for {side}.{type} required in quote asset");
return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in quote asset");
if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null)
return new ArgumentError($"Quantity for {side}.{type} required in base asset");
return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base asset");
if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null)
return new ArgumentError($"Quantity for {side}.{type} required in contracts");
return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in contracts");
return null;
}
@@ -1,14 +1,27 @@
namespace CryptoExchange.Net.SharedApis
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Symbol request
/// </summary>
public record SharedSymbolRequest : SharedRequest
{
/// <summary>
/// Trading mode
/// </summary>
public TradingMode TradingMode { get; }
/// <summary>
/// The symbol
/// </summary>
public SharedSymbol Symbol { get; set; }
public SharedSymbol? Symbol { get; set; }
/// <summary>
/// Symbols
/// </summary>
public SharedSymbol[]? Symbols { get; set; }
/// <summary>
/// ctor
@@ -16,6 +29,22 @@
public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
{
Symbol = symbol;
TradingMode = symbol.TradingMode;
}
/// <summary>
/// ctor
/// </summary>
public SharedSymbolRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
{
if (!symbols.Any())
throw new ArgumentException("Empty symbol list");
if (symbols.GroupBy(x => x.TradingMode).Count() > 1)
throw new ArgumentException("All symbols in the symbol list should have the same trading mode");
Symbols = symbols.ToArray();
TradingMode = Symbols.First().TradingMode;
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to book ticker updates
@@ -13,5 +15,22 @@
public SubscribeBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeBookTickerRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeBookTickerRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to kline/candlestick updates
@@ -20,5 +22,26 @@
{
Interval = interval;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="interval">Kline interval</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeKlineRequest(IEnumerable<SharedSymbol> symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
Interval = interval;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="interval">Kline interval</param>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeKlineRequest(SharedKlineInterval interval, params SharedSymbol[] symbols) : base(symbols, null)
{
Interval = interval;
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to order book snapshot updates
@@ -20,5 +22,22 @@
{
Limit = limit;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeOrderBookRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeOrderBookRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -1,4 +1,7 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to ticker updates
@@ -13,5 +16,22 @@
public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeTickerRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeTickerRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to trade updates
@@ -13,5 +15,22 @@
public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeTradeRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeTradeRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -30,5 +30,8 @@ namespace CryptoExchange.Net.SharedApis
public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType)
{
}
/// <inheritdoc />
public override SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant(), DeliveryTime) { SymbolName = Name };
}
}
@@ -7,6 +7,10 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public record SharedPosition : SharedSymbolModel
{
/// <summary>
/// Position id
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Current size of the position
/// </summary>
@@ -69,5 +69,11 @@
Name = symbol;
Trading = trading;
}
/// <summary>
/// The SharedSymbol of this symbol
/// </summary>
/// <returns></returns>
public virtual SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant()) { SymbolName = Name };
}
}
@@ -1,6 +1,8 @@
using System;
using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.SharedApis
{
@@ -25,7 +27,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
protected SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
internal SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
{
QuantityInBaseAsset = baseAssetQuantity;
QuantityInQuoteAsset = quoteAssetQuantity;
@@ -36,6 +38,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Quantity for an order
/// </summary>
[JsonConverter(typeof(SharedQuantityConverter))]
public record SharedQuantity : SharedQuantityReference
{
private SharedQuantity(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity)
@@ -43,6 +46,11 @@ namespace CryptoExchange.Net.SharedApis
{
}
/// <summary>
/// ctor
/// </summary>
public SharedQuantity() : base(null, null, null) { }
/// <summary>
/// Specify quantity in base asset
/// </summary>
@@ -98,6 +106,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Order quantity
/// </summary>
[JsonConverter(typeof(SharedOrderQuantityConverter))]
public record SharedOrderQuantity : SharedQuantityReference
{
/// <summary>
@@ -1,12 +1,15 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// A symbol representation based on a base and quote asset
/// </summary>
[JsonConverter(typeof(SharedSymbolConverter))]
public record SharedSymbol
{
/// <summary>
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting;
using Microsoft.Extensions.Logging;
@@ -252,6 +253,11 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message, we));
}
if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized)
{
return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected")));
}
#else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead
@@ -377,7 +383,19 @@ namespace CryptoExchange.Net.Sockets
var bytes = Parameters.Encoding.GetBytes(data);
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
_sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Text, Weight = weight, Bytes = bytes });
_sendEvent.Set();
return true;
}
/// <inheritdoc />
public virtual bool Send(int id, byte[] data, int weight)
{
if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing)
return false;
_logger.SocketAddingBytesToSendBuffer(Id, id, data);
_sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Binary, Weight = weight, Bytes = data });
_sendEvent.Set();
return true;
}
@@ -532,7 +550,7 @@ namespace CryptoExchange.Net.Sockets
try
{
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), data.Type, true, _ctsSource.Token).ConfigureAwait(false);
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
_logger.SocketSentBytes(Id, data.Id, data.Bytes.Length);
}
@@ -858,6 +876,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public DateTime SendTime { get; set; }
/// <summary>
/// Message type
/// </summary>
public WebSocketMessageType Type { get; set; }
/// <summary>
/// The bytes to send
/// </summary>
@@ -0,0 +1,180 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Message link type
/// </summary>
public enum MessageLinkType
{
/// <summary>
/// Match when the listen id matches fully to the value
/// </summary>
Full,
/// <summary>
/// Match when the listen id starts with the value
/// </summary>
StartsWith
}
/// <summary>
/// Matches a message listen id to a specific listener
/// </summary>
public class MessageMatcher
{
/// <summary>
/// Linkers in this matcher
/// </summary>
public MessageHandlerLink[] HandlerLinks { get; }
/// <summary>
/// ctor
/// </summary>
private MessageMatcher(params MessageHandlerLink[] links)
{
HandlerLinks = links;
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(string value)
{
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, (con, msg) => new CallResult<T>(default, msg.OriginalData, null)));
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(string value, Func<SocketConnection, DataEvent<T>, CallResult> handler)
{
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, handler));
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(IEnumerable<string> values, Func<SocketConnection, DataEvent<T>, CallResult> handler)
{
return new MessageMatcher(values.Select(x => new MessageHandlerLink<T>(MessageLinkType.Full, x, handler)).ToArray());
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(MessageLinkType type, string value, Func<SocketConnection, DataEvent<T>, CallResult> handler)
{
return new MessageMatcher(new MessageHandlerLink<T>(type, value, handler));
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create(params MessageHandlerLink[] linkers)
{
return new MessageMatcher(linkers);
}
/// <summary>
/// Whether this matcher contains a specific link
/// </summary>
public bool ContainsCheck(MessageHandlerLink link) => HandlerLinks.Any(x => x.Type == link.Type && x.Value == link.Value);
/// <summary>
/// Get any handler links matching with the listen id
/// </summary>
public List<MessageHandlerLink> GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId)).ToList();
/// <inheritdoc />
public override string ToString() => string.Join(",", HandlerLinks.Select(x => x.ToString()));
}
/// <summary>
/// Message handler link
/// </summary>
public abstract class MessageHandlerLink
{
/// <summary>
/// Type of check
/// </summary>
public MessageLinkType Type { get; }
/// <summary>
/// String value of the check
/// </summary>
public string Value { get; }
/// <summary>
/// Deserialization type
/// </summary>
public abstract Type GetDeserializationType(IMessageAccessor accessor);
/// <summary>
/// ctor
/// </summary>
public MessageHandlerLink(MessageLinkType type, string value)
{
Type = type;
Value = value;
}
/// <summary>
/// Whether this listen id matches this link
/// </summary>
public bool Check(string listenId)
{
if (Type == MessageLinkType.Full)
return Value.Equals(listenId, StringComparison.Ordinal);
return listenId.StartsWith(Value, StringComparison.Ordinal);
}
/// <summary>
/// Message handler
/// </summary>
public abstract CallResult Handle(SocketConnection connection, DataEvent<object> message);
/// <inheritdoc />
public override string ToString() => $"{Type} match for \"{Value}\"";
}
/// <summary>
/// Message handler link
/// </summary>
public class MessageHandlerLink<TServer>: MessageHandlerLink
{
private Func<SocketConnection, DataEvent<TServer>, CallResult> _handler;
/// <inheritdoc />
public override Type GetDeserializationType(IMessageAccessor accessor) => typeof(TServer);
/// <summary>
/// ctor
/// </summary>
public MessageHandlerLink(string value, Func<SocketConnection, DataEvent<TServer>, CallResult> handler)
: this(MessageLinkType.Full, value, handler)
{
}
/// <summary>
/// ctor
/// </summary>
public MessageHandlerLink(MessageLinkType type, string value, Func<SocketConnection, DataEvent<TServer>, CallResult> handler)
: base(type, value)
{
_handler = handler;
}
/// <inheritdoc />
public override CallResult Handle(SocketConnection connection, DataEvent<object> message)
{
return _handler(connection, message.As((TServer)message.Data));
}
}
}
+22 -64
View File
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -28,6 +29,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
/// <summary>
/// What should happen if the query times out
/// </summary>
public TimeoutBehavior TimeoutBehavior { get; set; } = TimeoutBehavior.Fail;
/// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response
@@ -60,9 +66,9 @@ namespace CryptoExchange.Net.Sockets
public AsyncResetEvent? ContinueAwaiter { get; set; }
/// <summary>
/// Strings to match this query to a received message
/// Matcher for this query
/// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; }
public MessageMatcher MessageMatcher { get; set; } = null!;
/// <summary>
/// The query request object
@@ -84,13 +90,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool ExpectsResponse { get; set; } = true;
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// Wait event for response
/// </summary>
@@ -161,23 +160,16 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Handle a response message
/// </summary>
/// <param name="message"></param>
/// <param name="connection"></param>
/// <returns></returns>
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check);
}
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TServerResponse">The type returned from the server</typeparam>
/// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
public abstract class Query<TServerResponse, THandlerResponse> : Query
public abstract class Query<THandlerResponse> : Query
{
/// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
/// <summary>
/// The typed call result
/// </summary>
@@ -194,25 +186,22 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check)
{
var typedMessage = message.As((TServerResponse)message.Data);
if (!ValidateMessage(typedMessage))
if (!PreCheckMessage(connection, message))
return CallResult.SuccessResult;
CurrentResponses++;
if (CurrentResponses == RequiredResponses)
{
Completed = true;
if (CurrentResponses == RequiredResponses)
Response = message.Data;
}
if (Result?.Success != false)
// If an error result is already set don't override that
Result = HandleMessage(connection, typedMessage);
Result = check.Handle(connection, message);
if (CurrentResponses == RequiredResponses)
{
Completed = true;
_event.Set();
if (ContinueAwaiter != null)
await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
@@ -224,26 +213,20 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Validate if a message is actually processable by this query
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public virtual bool ValidateMessage(DataEvent<TServerResponse> message) => true;
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult<THandlerResponse> HandleMessage(SocketConnection connection, DataEvent<TServerResponse> message);
public virtual bool PreCheckMessage(SocketConnection connection, DataEvent<object> message) => true;
/// <inheritdoc />
public override void Timeout()
{
if (Completed)
return;
Completed = true;
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
if (TimeoutBehavior == TimeoutBehavior.Fail)
Result = new CallResult<THandlerResponse>(new TimeoutError());
else
Result = new CallResult<THandlerResponse>(default, null, default);
ContinueAwaiter?.Set();
_event.Set();
}
@@ -257,29 +240,4 @@ namespace CryptoExchange.Net.Sockets
_event.Set();
}
}
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query<TResponse, TResponse>
{
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
}
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public override CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => message.ToCallResult();
}
}
+170 -95
View File
@@ -11,8 +11,6 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions;
using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets
{
@@ -204,6 +202,18 @@ namespace CryptoExchange.Net.Sockets
}
}
/// <summary>
/// The number of current pending requests
/// </summary>
public int PendingRequests
{
get
{
lock (_listenersLock)
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
}
}
private bool _pausedActivity;
private readonly object _listenersLock;
private readonly List<IMessageProcessor> _listeners;
@@ -211,7 +221,8 @@ namespace CryptoExchange.Net.Sockets
private SocketStatus _status;
private readonly IMessageSerializer _serializer;
private readonly IByteMessageAccessor _accessor;
private IByteMessageAccessor? _stringMessageAccessor;
private IByteMessageAccessor? _byteMessageAccessor;
/// <summary>
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary.
@@ -228,6 +239,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
private readonly IWebsocket _socket;
/// <summary>
/// Cache for deserialization, only caches for a single message
/// </summary>
private readonly Dictionary<Type, object> _deserializationCache = new Dictionary<Type, object>();
/// <summary>
/// New socket connection
/// </summary>
@@ -258,7 +274,6 @@ namespace CryptoExchange.Net.Sockets
_listeners = new List<IMessageProcessor>();
_serializer = apiClient.CreateSerializer();
_accessor = apiClient.CreateAccessor();
}
/// <summary>
@@ -446,9 +461,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Handle a message
/// </summary>
/// <param name="data"></param>
/// <param name="type"></param>
/// <returns></returns>
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
{
var sw = Stopwatch.StartNew();
@@ -459,111 +471,128 @@ namespace CryptoExchange.Net.Sockets
data = ApiClient.PreprocessStreamMessage(this, type, data);
// 2. Read data into accessor
_accessor.Read(data);
IByteMessageAccessor accessor;
if (type == WebSocketMessageType.Binary)
accessor = _stringMessageAccessor ??= ApiClient.CreateAccessor(type);
else
accessor = _byteMessageAccessor ??= ApiClient.CreateAccessor(type);
var result = accessor.Read(data);
try
{
bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData;
if (outputOriginalData)
{
originalData = _accessor.GetOriginalString();
originalData = accessor.GetOriginalString();
_logger.ReceivedData(SocketId, originalData);
}
// 3. Determine the identifying properties of this message
var listenId = ApiClient.GetListenerIdentifier(_accessor);
if (listenId == null)
if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages)
{
originalData = outputOriginalData ? _accessor.GetOriginalString() : "[OutputOriginalData is false]";
if (!ApiClient.UnhandledMessageExpected)
_logger.FailedToEvaluateMessage(SocketId, originalData);
UnhandledMessage?.Invoke(_accessor);
_logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!);
return;
}
// 4. Get the listeners interested in this message
List<IMessageProcessor> processors;
lock (_listenersLock)
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
// 3. Determine the identifying properties of this message
var listenId = ApiClient.GetListenerIdentifier(accessor);
if (listenId == null)
{
originalData ??= "[OutputOriginalData is false]";
if (!ApiClient.UnhandledMessageExpected)
_logger.FailedToEvaluateMessage(SocketId, originalData);
if (processors.Count == 0)
UnhandledMessage?.Invoke(accessor);
return;
}
bool processed = false;
var totalUserTime = 0;
List<IMessageProcessor> localListeners;
lock(_listenersLock)
localListeners = _listeners.ToList();
foreach(var processor in localListeners)
{
foreach(var listener in processor.MessageMatcher.GetHandlerLinks(listenId))
{
processed = true;
_logger.ProcessorMatched(SocketId, listener.ToString(), listenId);
// 4. Determine the type to deserialize to for this processor
var messageType = listener.GetDeserializationType(accessor);
if (messageType == null)
{
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
continue;
}
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
{
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
subscriptionProcessor.Confirmed = true;
if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed)
// If this subscription has a query waiting for a timeout (success if there is no error response)
// then time it out now as the data is being received, so we assume it's successful
subscriptionProcessor.SubscriptionQuery.Timeout();
}
// 5. Deserialize the message
_deserializationCache.TryGetValue(messageType, out var deserialized);
if (deserialized == null)
{
var desResult = processor.Deserialize(accessor, messageType);
if (!desResult)
{
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
continue;
}
deserialized = desResult.Data;
_deserializationCache.Add(messageType, deserialized);
}
// 6. Pass the message to the handler
try
{
var innerSw = Stopwatch.StartNew();
await processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null), listener).ConfigureAwait(false);
if (processor is Query query && query.RequiredResponses != 1)
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
totalUserTime += (int)innerSw.ElapsedMilliseconds;
}
catch (Exception ex)
{
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
if (processor is Subscription subscription)
subscription.InvokeExceptionHandler(ex);
}
}
}
if (!processed)
{
if (!ApiClient.UnhandledMessageExpected)
{
List<string> listenerIds;
lock (_listenersLock)
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
listenerIds = _listeners.Select(l => l.MessageMatcher.ToString()).ToList();
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
UnhandledMessage?.Invoke(_accessor);
UnhandledMessage?.Invoke(accessor);
}
return;
}
_logger.ProcessorMatched(SocketId, processors.Count, listenId);
var totalUserTime = 0;
Dictionary<Type, object>? desCache = null;
if (processors.Count > 1)
{
// Only instantiate a cache if there are multiple processors
desCache = new Dictionary<Type, object>();
}
foreach (var processor in processors)
{
// 5. Determine the type to deserialize to for this processor
var messageType = processor.GetMessageType(_accessor);
if (messageType == null)
{
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
continue;
}
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
{
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
subscriptionProcessor.Confirmed = true;
// This doesn't trigger a waiting subscribe query, should probably also somehow set the wait event for that
}
// 6. Deserialize the message
object? deserialized = null;
desCache?.TryGetValue(messageType, out deserialized);
if (deserialized == null)
{
var desResult = processor.Deserialize(_accessor, messageType);
if (!desResult)
{
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
continue;
}
deserialized = desResult.Data;
desCache?.Add(messageType, deserialized);
}
// 7. Hand of the message to the subscription
try
{
var innerSw = Stopwatch.StartNew();
await processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null)).ConfigureAwait(false);
if (processor is Query query && query.RequiredResponses != 1)
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
totalUserTime += (int)innerSw.ElapsedMilliseconds;
}
catch (Exception ex)
{
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
if (processor is Subscription subscription)
subscription.InvokeExceptionHandler(ex);
}
}
_logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime);
}
finally
{
_accessor.Clear();
_deserializationCache.Clear();
accessor.Clear();
}
}
@@ -642,7 +671,7 @@ namespace CryptoExchange.Net.Sockets
bool anyDuplicateSubscription;
lock (_listenersLock)
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l)));
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l)));
bool shouldCloseConnection;
lock (_listenersLock)
@@ -751,22 +780,21 @@ namespace CryptoExchange.Net.Sockets
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
{
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.Result ?? new CallResult(new ServerError("Timeout"));
return query.Result ?? new CallResult(new TimeoutError());
}
/// <summary>
/// Send a query request and wait for an answer
/// </summary>
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<THandlerResponse>(Query<THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
{
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
return query.TypedResult ?? new CallResult<THandlerResponse>(new TimeoutError());
}
private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default)
@@ -825,8 +853,55 @@ namespace CryptoExchange.Net.Sockets
/// <param name="weight">The weight of the message</param>
public virtual CallResult Send<T>(int requestId, T obj, int weight)
{
var data = obj is string str ? str : _serializer.Serialize(obj!);
return Send(requestId, data, weight);
if (_serializer is IByteMessageSerializer byteSerializer)
{
return SendBytes(requestId, byteSerializer.Serialize(obj), weight);
}
else if (_serializer is IStringMessageSerializer stringSerializer)
{
if (obj is string str)
return Send(requestId, str, weight);
str = stringSerializer.Serialize(obj);
return Send(requestId, str, weight);
}
throw new Exception("Unknown serializer when sending message");
}
/// <summary>
/// Send byte data over the websocket connection
/// </summary>
/// <param name="data">The data to send</param>
/// <param name="weight">The weight of the message</param>
/// <param name="requestId">The id of the request</param>
public virtual CallResult SendBytes(int requestId, byte[] data, int weight)
{
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{
var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info));
}
if (!_socket.IsOpen)
{
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId);
return new CallResult(new WebError("Failed to send message, socket no longer open"));
}
_logger.SendingByteData(SocketId, requestId, data.Length);
try
{
if (!_socket.Send(requestId, data, weight))
return new CallResult(new WebError("Failed to send message, connection not open"));
return CallResult.SuccessResult;
}
catch (Exception ex)
{
return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex));
}
}
/// <summary>
@@ -839,7 +914,7 @@ namespace CryptoExchange.Net.Sockets
{
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info));
}
@@ -936,7 +1011,7 @@ namespace CryptoExchange.Net.Sockets
return result;
}
var subQuery = subscription.GetSubQuery(this);
var subQuery = subscription.CreateSubscriptionQuery(this);
if (subQuery == null)
{
subscription.IsResubscribing = false;
@@ -971,7 +1046,7 @@ namespace CryptoExchange.Net.Sockets
internal async Task UnsubscribeAsync(Subscription subscription)
{
var unsubscribeRequest = subscription.GetUnsubQuery();
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
if (unsubscribeRequest == null)
return;
@@ -984,7 +1059,7 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen)
return new CallResult(new WebError("Socket is not connected"));
var subQuery = subscription.GetSubQuery(this);
var subQuery = subscription.CreateSubscriptionQuery(this);
if (subQuery == null)
return CallResult.SuccessResult;
+40 -30
View File
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -60,9 +61,9 @@ namespace CryptoExchange.Net.Sockets
public bool Authenticated { get; }
/// <summary>
/// Strings to match this subscription to a received message
/// Matcher for this subscription
/// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; }
public MessageMatcher MessageMatcher { get; set; } = null!;
/// <summary>
/// Cancellation token registration
@@ -74,24 +75,24 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action<Exception>? Exception;
/// <summary>
/// Get the deserialization type for this message
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// Subscription topic
/// </summary>
public string? Topic { get; set; }
/// <summary>
/// The subscribe query for this subscription
/// </summary>
public Query? SubscriptionQuery { get; private set; }
/// <summary>
/// The unsubscribe query for this subscription
/// </summary>
public Query? UnsubscriptionQuery { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
/// <param name="userSubscription"></param>
public Subscription(ILogger logger, bool authenticated, bool userSubscription = true)
{
_logger = logger;
@@ -100,11 +101,21 @@ namespace CryptoExchange.Net.Sockets
Id = ExchangeHelpers.NextId();
}
/// <summary>
/// Create a new subscription query
/// </summary>
public Query? CreateSubscriptionQuery(SocketConnection connection)
{
var query = GetSubQuery(connection);
SubscriptionQuery = query;
return query;
}
/// <summary>
/// Get the subscribe query to send when subscribing
/// </summary>
/// <returns></returns>
public abstract Query? GetSubQuery(SocketConnection connection);
protected abstract Query? GetSubQuery(SocketConnection connection);
/// <summary>
/// Handle a subscription query response
@@ -118,11 +129,21 @@ namespace CryptoExchange.Net.Sockets
/// <param name="message"></param>
public virtual void HandleUnsubQueryResponse(object message) { }
/// <summary>
/// Create a new unsubscription query
/// </summary>
public Query? CreateUnsubscriptionQuery(SocketConnection connection)
{
var query = GetUnsubQuery(connection);
UnsubscriptionQuery = query;
return query;
}
/// <summary>
/// Get the unsubscribe query to send when unsubscribing
/// </summary>
/// <returns></returns>
public abstract Query? GetUnsubQuery();
protected abstract Query? GetUnsubQuery(SocketConnection connection);
/// <inheritdoc />
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
@@ -130,14 +151,11 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Handle an update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matcher)
{
ConnectionInvocations++;
TotalInvocations++;
return Task.FromResult(DoHandleMessage(connection, message));
return Task.FromResult(matcher.Handle(connection, message));
}
/// <summary>
@@ -154,14 +172,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public virtual void DoHandleReset() { }
/// <summary>
/// Handle the update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Invoke the exception event
/// </summary>
@@ -177,12 +187,12 @@ namespace CryptoExchange.Net.Sockets
/// <param name="Id">The id of the subscription</param>
/// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param>
/// <param name="Invocations">Number of times this subscription got a message</param>
/// <param name="Identifiers">Identifiers the subscription is listening to</param>
/// <param name="ListenMatcher">Matcher for this subscription</param>
public record SubscriptionState(
int Id,
bool Confirmed,
int Invocations,
HashSet<string> Identifiers
MessageMatcher ListenMatcher
);
/// <summary>
@@ -191,7 +201,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public SubscriptionState GetState()
{
return new SubscriptionState(Id, Confirmed, TotalInvocations, ListenerIdentifiers);
return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher);
}
}
@@ -22,37 +22,9 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public override Query? GetSubQuery(SocketConnection connection) => null;
protected override Query? GetSubQuery(SocketConnection connection) => null;
/// <inheritdoc />
public override Query? GetUnsubQuery() => null;
}
/// <inheritdoc />
public abstract class SystemSubscription<T> : SystemSubscription
{
/// <inheritdoc />
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
/// <inheritdoc />
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
=> HandleMessage(connection, message.As((T)message.Data));
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
protected SystemSubscription(ILogger logger, bool authenticated) : base(logger, authenticated)
{
}
/// <summary>
/// Handle an update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult HandleMessage(SocketConnection connection, DataEvent<T> message);
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
}
@@ -386,7 +386,7 @@ namespace CryptoExchange.Net.Testing.Comparers
var stringValue = jsonValue.GetString();
if (objectValue is decimal dec)
{
if (decimal.Parse(stringValue!, CultureInfo.InvariantCulture) != dec)
if (ExchangeHelpers.ParseDecimal(stringValue!) != dec)
throw new Exception($"{method}: {property} not equal: {stringValue} vs {dec}");
}
else if (objectValue is DateTime time)
@@ -67,6 +67,17 @@ namespace CryptoExchange.Net.Testing.Implementations
return true;
}
public bool Send(int requestId, byte[] data, int weight)
{
if (!Connected)
throw new Exception("Socket not connected");
OnRequestSent?.Invoke(requestId);
OnMessageSend?.Invoke(Encoding.UTF8.GetString(data));
return true;
}
public Task CloseAsync()
{
Connected = false;
+17 -13
View File
@@ -128,23 +128,27 @@ namespace CryptoExchange.Net.Testing
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null;
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null;
var headers = new Dictionary<string, string>();
var requestDefinition = new RestRequestConfiguration(
new RequestDefinition(path, method)
{
Authenticated = true
},
host,
uriParams ?? new Dictionary<string, object>(),
bodyParams ?? new Dictionary<string, object>(),
new Dictionary<string, string>(),
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
);
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
authProvider.AuthenticateRequest(
client,
new Uri(host.AppendPath(path)),
method,
ref uriParams,
ref bodyParams,
ref headers,
true,
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
authProvider.ProcessRequest(
client,
requestDefinition
);
var signature = getSignature(uriParams, bodyParams, headers);
var signature = getSignature(requestDefinition.QueryParameters, requestDefinition.BodyParameters, requestDefinition.Headers);
if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
@@ -176,18 +176,32 @@ namespace CryptoExchange.Net.Trackers.Klines
Status = SyncStatus.Syncing;
_logger.KlineTrackerStarting(SymbolName);
var startResult = await DoStartAsync().ConfigureAwait(false);
if (!startResult)
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
update =>
{
AddOrUpdate(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{
_logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.Message, startResult.Error.Exception);
_logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception);
Status = SyncStatus.Disconnected;
return new CallResult(startResult.Error!);
return subResult;
}
_updateSubscription = startResult.Data;
_updateSubscription = subResult.Data;
_updateSubscription.ConnectionLost += HandleConnectionLost;
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
var startResult = await DoStartAsync().ConfigureAwait(false);
if (!startResult)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return new CallResult(startResult.Error!);
}
Status = SyncStatus.Synced;
_logger.KlineTrackerStarted(SymbolName);
return CallResult.SuccessResult;
@@ -208,22 +222,10 @@ namespace CryptoExchange.Net.Trackers.Klines
/// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot
/// </summary>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync()
protected virtual async Task<CallResult> DoStartAsync()
{
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
update =>
{
AddOrUpdate(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{
Status = SyncStatus.Disconnected;
return subResult;
}
if (!_startWithSnapshot)
return subResult;
return CallResult.SuccessResult;
var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value);
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
@@ -236,11 +238,7 @@ namespace CryptoExchange.Net.Trackers.Klines
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
{
if (!result)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return subResult.AsError<UpdateSubscription>(result.Error!);
}
return result;
if (Limit != null && data.Count > Limit)
break;
@@ -249,7 +247,7 @@ namespace CryptoExchange.Net.Trackers.Klines
}
SetInitialData(data);
return subResult;
return CallResult.SuccessResult;
}
/// <summary>
@@ -199,10 +199,15 @@ namespace CryptoExchange.Net.Trackers.Trades
_startWithSnapshot = startWithSnapshot;
Status = SyncStatus.Syncing;
_logger.TradeTrackerStarting(SymbolName);
var subResult = await DoStartAsync().ConfigureAwait(false);
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
update =>
{
AddData(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception);
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception);
Status = SyncStatus.Disconnected;
return subResult;
}
@@ -211,6 +216,15 @@ namespace CryptoExchange.Net.Trackers.Trades
_updateSubscription.ConnectionLost += HandleConnectionLost;
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
var result = await DoStartAsync().ConfigureAwait(false);
if (!result)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return result;
}
SetSyncStatus();
_logger.TradeTrackerStarted(SymbolName);
return CallResult.SuccessResult;
@@ -231,22 +245,10 @@ namespace CryptoExchange.Net.Trackers.Trades
/// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot
/// </summary>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync()
protected virtual async Task<CallResult> DoStartAsync()
{
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
update =>
{
AddData(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{
Status = SyncStatus.Disconnected;
return subResult;
}
if (!_startWithSnapshot)
return subResult;
return CallResult.SuccessResult;
if (_historyRestClient != null)
{
@@ -256,12 +258,8 @@ namespace CryptoExchange.Net.Trackers.Trades
await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
{
if (!result)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return subResult.AsError<UpdateSubscription>(result.Error!);
}
return result;
if (Limit != null && data.Count > Limit)
break;
@@ -279,15 +277,13 @@ namespace CryptoExchange.Net.Trackers.Trades
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false);
if (!snapshot)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return subResult.AsError<UpdateSubscription>(snapshot.Error!);
return snapshot;
}
SetInitialData(snapshot.Data);
}
return subResult;
return CallResult.SuccessResult;
}
/// <summary>
+1 -1
View File
@@ -1,4 +1,4 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
+22 -20
View File
@@ -5,27 +5,29 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="11.0.0" />
<PackageReference Include="Bitfinex.Net" Version="9.0.0" />
<PackageReference Include="BitMart.Net" Version="2.0.0" />
<PackageReference Include="Bybit.Net" Version="5.0.0" />
<PackageReference Include="CoinEx.Net" Version="9.0.1" />
<PackageReference Include="CryptoCom.Net" Version="2.0.0" />
<PackageReference Include="DeepCoin.Net" Version="2.0.0" />
<PackageReference Include="GateIo.Net" Version="2.0.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.0.0" />
<PackageReference Include="JK.BingX.Net" Version="2.0.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.0.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.0.0" />
<PackageReference Include="JK.OKX.Net" Version="3.0.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.0.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.0.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.0.0" />
<PackageReference Include="Kucoin.Net" Version="7.0.0" />
<PackageReference Include="Binance.Net" Version="11.3.0" />
<PackageReference Include="Bitfinex.Net" Version="9.3.0" />
<PackageReference Include="BitMart.Net" Version="2.4.1" />
<PackageReference Include="Bybit.Net" Version="5.4.0" />
<PackageReference Include="CoinEx.Net" Version="9.3.0" />
<PackageReference Include="CoinW.Net" Version="1.0.1" />
<PackageReference Include="CryptoCom.Net" Version="2.4.0" />
<PackageReference Include="DeepCoin.Net" Version="2.3.0" />
<PackageReference Include="GateIo.Net" Version="2.4.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.4.0" />
<PackageReference Include="JK.BingX.Net" Version="2.3.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.3.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.3.0" />
<PackageReference Include="JK.OKX.Net" Version="3.3.1" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.3.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.3.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.3.1" />
<PackageReference Include="Kucoin.Net" Version="7.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="WhiteBit.Net" Version="2.0.0" />
<PackageReference Include="XT.Net" Version="2.0.0" />
<PackageReference Include="Toobit.Net" Version="1.2.1" />
<PackageReference Include="WhiteBit.Net" Version="2.4.0" />
<PackageReference Include="XT.Net" Version="2.3.1" />
</ItemGroup>
</Project>
+12 -2
View File
@@ -8,6 +8,7 @@
@inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient
@inject ICoinExRestClient coinexClient
@inject ICoinWRestClient coinWClient
@inject ICryptoComRestClient cryptocomClient
@inject IDeepCoinRestClient deepCoinClient
@inject IGateIoRestClient gateioClient
@@ -17,6 +18,7 @@
@inject IKucoinRestClient kucoinClient
@inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient
@inject IToobitRestClient toobitClient
@inject IWhiteBitRestClient whitebitClient
@inject IXTRestClient xtClient
@@ -40,6 +42,7 @@
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync();
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
@@ -48,9 +51,10 @@
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("eth_btc");
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
@@ -81,6 +85,9 @@
if (coinexTask.Result.Success)
_prices.Add("CoinEx", coinexTask.Result.Data.Single().LastPrice);
if (coinWTask.Result.Success)
_prices.Add("CoinW", coinWTask.Result.Data.Single(x => x.Symbol == "BTC_USDT").LastPrice);
if (cryptocomTask.Result.Success)
_prices.Add("CryptoCom", cryptocomTask.Result.Data.First().LastPrice ?? 0);
@@ -116,6 +123,9 @@
if (okxTask.Result.Success)
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
if (toobitTask.Result.Success)
_prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0);
if (whitebitTask.Result.Success){
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
var tickers = whitebitTask.Result.Data;
@@ -8,6 +8,7 @@
@inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient
@inject ICoinExSocketClient coinExSocketClient
@inject ICoinWSocketClient coinWSocketClient
@inject ICryptoComSocketClient cryptocomSocketClient
@inject IDeepCoinSocketClient deepCoinSocketClient
@inject IGateIoSocketClient gateioSocketClient
@@ -17,6 +18,7 @@
@inject IKucoinSocketClient kucoinSocketClient
@inject IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient
@inject IToobitSocketClient toobitSocketClient
@inject IWhiteBitSocketClient whitebitSocketClient
@inject IXTSocketClient xtSocketClient
@using System.Collections.Concurrent
@@ -48,6 +50,7 @@
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)),
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
@@ -60,6 +63,8 @@
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
// Toobit doesn't support the ETH/BTC pair
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
};
+8 -1
View File
@@ -9,6 +9,7 @@
@using BitMEX.Net.Interfaces;
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Authentication
@using CryptoExchange.Net.Interfaces
@@ -22,6 +23,7 @@
@using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces
@using OKX.Net.Interfaces;
@using Toobit.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces
@inject IBinanceOrderBookFactory binanceFactory
@@ -33,6 +35,7 @@
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject ICoinWOrderBookFactory coinWFactory
@inject ICryptoComOrderBookFactory cryptocomFactory
@inject IDeepCoinOrderBookFactory deepCoinFactory
@inject IGateIoOrderBookFactory gateioFactory
@@ -42,6 +45,7 @@
@inject IKucoinOrderBookFactory kucoinFactory
@inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory
@inject IToobitOrderBookFactory toobitFactory
@inject IWhiteBitOrderBookFactory whitebitFactory
@inject IXTOrderBookFactory xtFactory
@implements IDisposable
@@ -86,6 +90,7 @@
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
// DeepCoin does not support the ETH/BTC pair
@@ -97,6 +102,8 @@
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") },
// Toobit does not support the ETH/BTC pair
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
{ "XT", xtFactory.CreateSpot("eth_btc") },
};
@@ -111,7 +118,7 @@
public void Dispose()
{
_timer.Stop();
_timer?.Stop();
_timer.Dispose();
foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
// It's not necessary to wait for this
+8 -2
View File
@@ -9,6 +9,7 @@
@using BitMart.Net.Interfaces;
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Interfaces
@using CryptoCom.Net.Interfaces
@@ -23,6 +24,7 @@
@using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces
@using OKX.Net.Interfaces;
@using Toobit.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces
@inject IBinanceTrackerFactory binanceFactory
@@ -34,6 +36,7 @@
@inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory
@inject ICoinExTrackerFactory coinExFactory
@inject ICoinWTrackerFactory coinWFactory
@inject ICryptoComTrackerFactory cryptocomFactory
@inject IDeepCoinTrackerFactory deepCoinFactory
@inject IGateIoTrackerFactory gateioFactory
@@ -43,6 +46,7 @@
@inject IKucoinTrackerFactory kucoinFactory
@inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory
@inject IToobitTrackerFactory toobitFactory
@inject IWhiteBitTrackerFactory whitebitFactory
@inject IXTTrackerFactory xtFactory
@implements IDisposable
@@ -80,6 +84,7 @@
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinWFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
@@ -90,6 +95,7 @@
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
};
@@ -118,8 +124,8 @@
public void Dispose()
{
_timer.Stop();
_timer.Dispose();
_timer?.Stop();
_timer?.Dispose();
foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected))
// It's not necessary to wait for this
_ = tracker.StopAsync();
+2
View File
@@ -41,6 +41,7 @@ namespace BlazorClient
services.AddBybit();
services.AddCoinbase();
services.AddCoinEx();
services.AddCoinW();
services.AddCryptoCom();
services.AddDeepCoin();
services.AddGateIo();
@@ -50,6 +51,7 @@ namespace BlazorClient
services.AddKucoin();
services.AddMexc();
services.AddOKX();
services.AddToobit();
services.AddWhiteBit();
services.AddXT();
}
+2
View File
@@ -17,6 +17,7 @@
@using Bybit.Net.Interfaces.Clients;
@using Coinbase.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients;
@using CoinW.Net.Interfaces.Clients;
@using CryptoCom.Net.Interfaces.Clients;
@using DeepCoin.Net.Interfaces.Clients;
@using GateIo.Net.Interfaces.Clients;
@@ -26,6 +27,7 @@
@using Kucoin.Net.Interfaces.Clients;
@using Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients;
@using WhiteBit.Net.Interfaces.Clients
@using XT.Net.Interfaces.Clients
@using CryptoExchange.Net.Interfaces;
+14 -14
View File
@@ -6,20 +6,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="11.0.0" />
<PackageReference Include="Bitfinex.Net" Version="9.0.0" />
<PackageReference Include="BitMart.Net" Version="2.0.0" />
<PackageReference Include="Bybit.Net" Version="5.0.0" />
<PackageReference Include="CoinEx.Net" Version="9.0.0" />
<PackageReference Include="CryptoCom.Net" Version="2.0.0" />
<PackageReference Include="GateIo.Net" Version="2.0.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.0.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.0.0" />
<PackageReference Include="JK.OKX.Net" Version="3.0.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.0.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.0.0" />
<PackageReference Include="Kucoin.Net" Version="7.0.0" />
<PackageReference Include="Binance.Net" Version="11.1.0" />
<PackageReference Include="Bitfinex.Net" Version="9.1.0" />
<PackageReference Include="BitMart.Net" Version="2.1.0" />
<PackageReference Include="Bybit.Net" Version="5.1.0" />
<PackageReference Include="CoinEx.Net" Version="9.1.0" />
<PackageReference Include="CryptoCom.Net" Version="2.1.0" />
<PackageReference Include="GateIo.Net" Version="2.1.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" />
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" />
<PackageReference Include="Kucoin.Net" Version="7.1.0" />
</ItemGroup>
</Project>
+3 -3
View File
@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="11.0.0" />
<PackageReference Include="BitMart.Net" Version="2.0.0" />
<PackageReference Include="JK.OKX.Net" Version="3.0.0" />
<PackageReference Include="Binance.Net" Version="11.1.0" />
<PackageReference Include="BitMart.Net" Version="2.1.0" />
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
</ItemGroup>
</Project>
+47
View File
@@ -21,6 +21,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|![CoinEx](https://raw.githubusercontent.com/JKorf/CoinEx.Net/refs/heads/master/CoinEx.Net/Icon/icon.png)|CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
|![CoinW](https://raw.githubusercontent.com/JKorf/CoinW.Net/refs/heads/main/CoinW.Net/Icon/icon.png)|CoinW|CEX|[JKorf/CoinW.Net](https://github.com/JKorf/CoinW.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinW.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinW.Net)|[Link](https://www.coinw.com/register?rc=rbtnp)|-|
|![CoinGecko](https://raw.githubusercontent.com/JKorf/CoinGecko.Net/refs/heads/main/CoinGecko.Net/Icon/icon.png)|CoinGecko|-|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|-|-|
|![Crypto.com](https://raw.githubusercontent.com/JKorf/CryptoCom.Net/refs/heads/main/CryptoCom.Net/Icon/icon.png)|Crypto.com|CEX|[JKorf/CryptoCom.Net](https://github.com/JKorf/CryptoCom.Net)|[![Nuget version](https://img.shields.io/nuget/v/CryptoCom.net.svg?style=flat-square)](https://www.nuget.org/packages/CryptoCom.Net)|[Link](https://crypto.com/exch/26ge92xbkn)|-|
|![DeepCoin](https://raw.githubusercontent.com/JKorf/DeepCoin.Net/refs/heads/main/DeepCoin.Net/Icon/icon.png)|DeepCoin|CEX|[JKorf/DeepCoin.Net](https://github.com/JKorf/DeepCoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/DeepCoin.net.svg?style=flat-square)](https://www.nuget.org/packages/DeepCoin.Net)|[Link](https://s.deepcoin.com/jddhfca)|-|
@@ -31,6 +32,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Kucoin](https://raw.githubusercontent.com/JKorf/Kucoin.Net/refs/heads/master/Kucoin.Net/Icon/icon.png)|Kucoin|CEX|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)|[Link](https://www.kucoin.com/r/rf/QBS4FPED)|-|
|![Mexc](https://raw.githubusercontent.com/JKorf/Mexc.Net/refs/heads/main/Mexc.Net/Icon/icon.png)|Mexc|CEX|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|-|-|
|![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|![WhiteBit](https://raw.githubusercontent.com/JKorf/WhiteBit.Net/refs/heads/main/WhiteBit.Net/Icon/icon.png)|WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-|
|![XT](https://raw.githubusercontent.com/JKorf/XT.Net/refs/heads/main/XT.Net/Icon/icon.png)|XT|CEX|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|[Link](https://www.xt.com/ru/accounts/register?ref=CZG39C)|25%|
@@ -57,6 +59,51 @@ 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).
## Release notes
* Version 9.6.0 - 25 Aug 2025
* Added support for parsing REST response even though status indicates error
* Added better support for subscriptions without subscribe confirmation
* Added check in websocket for receiving 401 unauthorized http response status when 101 was expected
* Removed obsolete attribute on Error.Code property, updated the description
* Version 9.5.0 - 19 Aug 2025
* Added better error handling support
* Added ErrorDescription, ErrorType and IsTransient to Error object
* Added ErrorCode in favor of Code
* Updated some error messages
* Refactored RestApiClient request authentication and AuthenticationProvider to prevent duplicate query string / body serialization
* Fixed IOrderBookSocketClient Shared interface not getting registered in DI
* Fixed response type in websocket queries not interested in the response
* Fixed timing issue in query response processing
* Version 9.4.0 - 04 Aug 2025
* Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported
* Version 9.3.1 - 29 Jul 2025
* Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum
* Added Id property to SharedPosition model
* Version 9.3.0 - 23 Jul 2025
* Updated websocket message to listener matching logic to be more flexible
* Updated decimal parser to support "NaN" and "-Infinity" strings, added check for negative overflow value, improved performance in most cases
* Version 9.2.1 - 16 Jul 2025
* Added setting for whether or not to process unparsable websocket messages
* Fixed issue causing duplicate subscriptions and data in the TradeTracker and KlineTracker when websocket connection was reconnected
* Version 9.2.0 - 14 Jul 2025
* Added support for sending byte data on websocket
* Added support for handling both string and byte data with different IMessageAccessor types
* Split IMessageSerializer into IByteMessageSerializer and IStringMessageSerializer
* Renamed IMessageAccessor.IsJson to IsValid
* Refactored ArrayConverter to remove separate converter options cache
* Version 9.1.0 - 28 May 2025
* Added JsonConverter implementation for SharedQuantity and SharedSymbol types, making usage of the types easier
* Updated dotnet dependency packages from 9.0.0 to 9.0.5
* Replaced Microsoft.Extensions.Logging.Abstractions with Microsoft.Extensions.Logging
* Replaced Microsoft.Extensions.Options.ConfigurationExtensions with Microsoft.Extensions.Configuration.Binder, which includes a source generator for AOT publishing
* Removed redundant Microsoft.Extensions.DependencyInjection.Abstractions package reference
* Version 9.0.1 - 20 May 2025
* Improved response time on CancellationToken cancel during subscribing
* Added support for sending query without expecting a response