1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 16:32:57 +00:00

Compare commits

..

107 Commits

Author SHA1 Message Date
Jkorf 8d5b6a53f3 Updated to version 9.12.0 2025-11-03 10:39:05 +01:00
Jkorf ad97102e7c Updated CryptoExchange.Net version 2025-11-03 10:38:53 +01:00
Jkorf d8dc121386 Updated to version 9.12.0 2025-11-03 09:48:29 +01:00
Jkorf 10c3868c00 Revert "Updated to version 9.11.0"
This reverts commit 411ba00a82.
2025-11-03 09:46:55 +01:00
Jkorf 411ba00a82 Updated to version 9.11.0 2025-11-03 09:45:41 +01:00
JKorf c1b2b62dbc Added AliasType to specify only one way conversion for AssetAliases 2025-11-02 15:17:44 +01:00
JKorf 3960cab7a7 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-11-02 11:41:30 +01:00
JKorf 34e9447e55 Added constant for selecting a USD asset for use in a Shared API/SharedSymbol 2025-11-02 11:41:26 +01:00
Jkorf 9d3295acc7 Removed some unhelpful verbose logs 2025-10-31 15:08:14 +01:00
Jkorf 995cd3d84c Updated to version 9.11.1 2025-10-30 14:33:26 +01:00
Jkorf 919cdf0075 Updated CryptoExchange.Net version 2025-10-30 14:32:41 +01:00
Jkorf d181c9cfc1 Updated to version 9.11.0 2025-10-30 14:21:30 +01:00
Jkorf 5943142c44 Updated to version 9.11.0 2025-10-30 13:47:48 +01:00
Jkorf dbc430e838 Added StaticLogger to LibraryHelpers, updated warning logging for converters to use StaticLogger 2025-10-30 12:52:05 +01:00
Jkorf 7413d03d31 Added client reference helper to LibraryHelpers 2025-10-30 11:25:27 +01:00
Jkorf dd60067684 Updated examples 2025-10-27 12:00:51 +01:00
Jkorf 04e4ddf525 Fixed exception when initial trade snapshot has no items in TradeTracker 2025-10-27 11:59:54 +01:00
Jkorf 99bf6d7c75 Added Upbit reference 2025-10-27 11:44:12 +01:00
Jkorf 99a203933c Added missing release notes 2025-10-15 13:59:10 +02:00
Jkorf b43d2a2040 Updated to version 9.10.0 2025-10-15 13:36:33 +02:00
Jkorf ba9c406def Updated CryptoExchange.Net version 2025-10-15 13:34:50 +02:00
Jkorf f5f4d50cc9 Updated to version 9.10.0 2025-10-15 13:24:25 +02:00
Jkorf f87506b490 Added ITransferRestClient, updated Shared IBalanceRestClient to use SharedAccountType 2025-10-15 13:21:00 +02:00
Jkorf f6f9a53ce5 Added ClientOrderId property to SharedUserTrade model 2025-10-13 15:42:27 +02:00
Jkorf 61130ef54e Added long overloads for parse methods in DateTimeConverter 2025-10-13 11:15:58 +02:00
Jkorf e8bcbd59be Updated DateTimeConverter to work primarily with decimal values instead of doubles to fix some floating point parsing issues 2025-10-13 09:06:14 +02:00
Jkorf d433ff7475 Updated to version 9.9.0 2025-10-06 13:47:21 +02:00
Jkorf 71957037d0 Updated CryptoExchange.Net version to 9.8.0 2025-10-06 13:45:47 +02:00
Jkorf bcdcdbbd4e Updated to version 9.9.0 2025-10-06 13:26:23 +02:00
Jkorf 1ece13f5bc Updated socket Subscription status handling, fixing timing issue for connection events and adding SubscriptionStatusChanged event 2025-10-06 13:22:40 +02:00
Jkorf da70ba6ec7 Added Aster reference 2025-10-06 10:37:28 +02:00
Jkorf a832f0e4d4 Updated to version 9.8.0 2025-09-30 12:06:14 +02:00
Jkorf 6ab4d005a0 Updated CryptoExchange.Net version to 9.8.0 2025-09-30 12:04:28 +02:00
Jkorf 18dc935038 Updated to version 9.8.0 2025-09-30 11:56:00 +02:00
Jkorf bb3a534f75 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-09-30 11:50:16 +02:00
nils2525 649ba370c6 Fixed EnumConverter to allow mapping empty string values (#253) 2025-09-30 11:49:58 +02:00
Jkorf 51732c5ce6 Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect 2025-09-29 14:43:39 +02:00
Jkorf 0ba7b46680 Added ContractAddress to SharedAsset model 2025-09-29 13:51:59 +02:00
Jkorf 94dfbb7b9e Fixed ExchangeHelpers.AdjustValueStep high precision calculation 2025-09-29 13:51:46 +02:00
Jkorf b8b7512b35 Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed 2025-09-29 10:07:28 +02:00
Jkorf aba6b773ce Added ITrackerFactory interface 2025-09-17 10:55:48 +02:00
Jkorf d88fb0d356 Added BloFin to ReadMe and examples 2025-09-17 10:01:17 +02:00
Jkorf b8c6d55156 CryptoManager.Net reference 2025-09-02 11:43:44 +02:00
Jkorf d9a5481db2 Updated to version 9.7.0 2025-09-01 13:37:16 +02:00
Jkorf 6a8bb42c0e Updated CryptoExchange.Net for CryptoExchange.Net.Protobuf version to 9.7.0 2025-09-01 13:35:24 +02:00
Jkorf 2445f001ab Updated to version 9.7.0 2025-09-01 13:18:16 +02:00
Jkorf c84fa9ac32 Fixed test 2025-09-01 13:16:20 +02:00
Jkorf d44a11c44e HttpVersion update
Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
Added REST client option for selecting HTTP protocol version
Added REST client option for HTTP client keep alive interval
Added HttpVersion to WebCallResult responses
Updated request logic to default to using HTTP version 2.0 for dotnet core
2025-09-01 10:12:59 +02:00
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
Jkorf 2cc2dc6ceb Updated to version 9.0.1 2025-05-20 14:51:19 +02:00
Jkorf 7da8cedf66 Improved response time on CancellationToken cancel during subscribing 2025-05-20 14:49:43 +02:00
Jkorf 2cf10668dd Added support for sending query without expecting a response 2025-05-19 16:06:45 +02:00
Jkorf f1342b5ff2 Examples 2025-05-14 14:30:05 +02:00
JKorf a04b636a11 Disable AOT warnings in testing code 2025-05-13 20:03:34 +02:00
Jkorf e4637ad295 Disable warning for AOT in testing helpers 2025-05-13 15:53:13 +02:00
Jkorf 3a1e43dabe Fixed framework check for setting IsAotCompatible project flag 2025-05-13 15:36:16 +02:00
Jkorf 10da1a7bfe Fix documentation link 2025-05-13 15:10:15 +02:00
Jkorf 37320ca862 Docs 2025-05-13 15:08:30 +02:00
Jkorf 2074a5e26f Updated to version 9.0.0 2025-05-13 10:28:12 +02:00
Jan Korf 6b14cdbf06 Feature/9.0.0 (#236)
* Added support for Native AOT compilation
* Updated all IEnumerable response types to array response types
* Added Pass support for ApiCredentials, removing the need for most implementations to add their own ApiCredentials type
* Added KeepAliveTimeout setting setting ping frame timeouts for SocketApiClient
* Added IBookTickerRestClient Shared interface for requesting book tickers
* Added ISpotTriggerOrderRestClient Shared interface for managing spot trigger orders
* Added ISpotOrderClientIdClient Shared interface for managing spot orders by client order id
* Added IFuturesTriggerOrderRestClient Shared interface for managing futures trigger orders
* Added IFuturesOrderClientIdClient Shared interface for managing futures orders by client order id
* Added IFuturesTpSlRestClient Shared interface for setting TP/SL on open futures positions
* Added GenerateClientOrderId to ISpotOrderRestClient and IFuturesOrderRestClient interface
* Added OptionalExchangeParameters and Supported properties to EndpointOptions
* Refactor Shared interfaces quantity parameters and properties to use SharedQuantity
* Added SharedSymbol property to Shared interface models returning a symbol
* Added TriggerPrice, IsTriggerOrder, TakeProfitPrice, StopLossPrice and IsCloseOrder to SharedFuturesOrder response model
* Added MaxShortLeverage and MaxLongLeverage to SharedFuturesSymbol response model
* Added StopLossPrice and TakeProfitPrice to SharedPosition response model
* Added TriggerPrice and IsTriggerOrder to SharedSpotOrder response model
* Added QuoteVolume property to SharedSpotTicker response model
* Added AssetAlias configuration models
* Added static ExchangeSymbolCache for tracking symbol information from exchanges
* Added static CallResult.SuccessResult to be used instead of constructing success CallResult instance
* Added static ApplyRules, RandomHexString and RandomLong helper methods to ExchangeHelpers class
* Added AsErrorWithData To CallResult
* Added OriginalData property to CallResult
* Added support for adjusting the rate limit key per call, allowing for ratelimiting depending on request parameters
* Added implementation for integration testing ISymbolOrderBook instances
* Added implementation for integration testing socket subscriptions
* Added implementation for testing socket queries
* Updated request cancellation logging to Debug level
* Updated logging SourceContext to include the client type
* Updated some logging logic, errors no longer contain any data, exception are not logged as string but instead forwarded to structured logging
* Fixed warning for Enum parsing throwing exception and output warnings for each object in a response to only once to prevent slowing down execution
* Fixed memory leak in AsyncAutoRestEvent
* Fixed logging for ping frame timeout
* Fixed warning getting logged when user stops SymbolOrderBook instance
* Fixed socket client `UnsubscribeAll` not unsubscribing dedicated connections
* Fixed memory leak in Rest client cache
* Fixed integers bigger than int16 not getting correctly parsed to enums
* Fixed issue where the default options were overridden when using SetApiCredentials
* Removed Newtonsoft.Json dependency
* Removed legacy Rest client code
* Removed legacy ISpotClient and IFuturesClient support
2025-05-13 10:15:30 +02:00
171 changed files with 5977 additions and 1312 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.12.0</PackageVersion>
<AssemblyVersion>9.12.0</AssemblyVersion>
<FileVersion>9.12.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.12.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>
+43
View File
@@ -0,0 +1,43 @@
# ![.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.12.0 - 03 Nov 2025
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.1 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.0 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.10.0 - 15 Oct 2025
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.9.0 - 06 Oct 2025
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
* Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0
* Version 9.4.0 - 04 Aug 2025
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Updated protobuf-net package version to 3.2.56
* Version 9.3.0 - 23 Jul 2025
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.2.0 - 14 Jul 2025
* Initial release
+18 -14
View File
@@ -1,9 +1,11 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@@ -16,9 +18,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 +38,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 +73,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 +86,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 +99,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);
@@ -112,6 +114,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
@@ -124,10 +127,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");
@@ -142,6 +145,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
@@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
@@ -80,8 +80,6 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.Message.Contains("Invalid request"));
Assert.That(result.Error.Message.Contains("123"));
}
[TestCase]
@@ -98,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");
}
@@ -184,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)
{
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
await sub;
// assert
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
}
[TestCase()]
@@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
await sub;
// assert
Assert.That(client.SubClient.TestSubscription.Confirmed);
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
}
}
}
@@ -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)]
@@ -269,9 +274,18 @@ namespace CryptoExchange.Net.UnitTests
TestInternal = new Test
{
Prop1 = 10
}
},
Prop8 = new Test3
{
Prop31 = 5,
Prop32 = "101"
},
};
var options = new JsonSerializerOptions()
{
TypeInfoResolver = new SerializationContext()
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
@@ -286,6 +300,42 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
@@ -323,7 +373,7 @@ namespace CryptoExchange.Net.UnitTests
public bool Value { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test, SerializationContext>))]
[JsonConverter(typeof(ArrayConverter<Test>))]
record Test
{
[ArrayProperty(0)]
@@ -344,9 +394,11 @@ namespace CryptoExchange.Net.UnitTests
public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test TestInternal { get; set; }
[ArrayProperty(8), JsonConversion]
public Test3 Prop8 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test2, SerializationContext>))]
[JsonConverter(typeof(ArrayConverter<Test2>))]
record Test2
{
[ArrayProperty(0)]
@@ -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,10 +10,12 @@ 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;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.UnitTests
{
@@ -24,12 +26,14 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test")
{
var options = new TestClientOptions();
_logger = NullLogger.Instance;
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
{
_logger = NullLogger.Instance;
Initialize(exchangeOptions);
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
}
@@ -52,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;
@@ -74,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
{
@@ -59,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
@@ -68,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
@@ -89,12 +90,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
}
@@ -117,13 +118,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
}
}
@@ -193,11 +194,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
{
var errorData = accessor.Deserialize<TestError>();
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 />
@@ -114,12 +115,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public CallResult ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub).Result;
return ConnectSocketAsync(sub, default).Result;
}
public override string GetListenerIdentifier(IMessageAccessor message)
{
if (!message.IsJson)
if (!message.IsValid)
{
return "topic";
}
+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);
}
}
+5 -6
View File
@@ -1,11 +1,13 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
namespace CryptoExchange.Net.Caching
{
internal class MemoryCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
private readonly object _lock = new object();
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
@@ -26,16 +28,13 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
if (DateTime.UtcNow - value.CacheTime > maxAge)
{
_cache.TryRemove(key, out _);
return null;
}
return value.Value;
}
+30 -9
View File
@@ -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;
@@ -39,7 +41,10 @@ namespace CryptoExchange.Net.Clients
public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Api options
@@ -51,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>
@@ -68,9 +78,10 @@ namespace CryptoExchange.Net.Clients
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
if (apiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
@@ -83,12 +94,22 @@ 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
{
ApiOptions.ApiCredentials = credentials;
if (credentials != null)
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <inheritdoc />
@@ -97,9 +118,9 @@ namespace CryptoExchange.Net.Clients
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
-4
View File
@@ -66,8 +66,6 @@ namespace CryptoExchange.Net.Clients
protected BaseClient(ILoggerFactory? logger, string exchange)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
Exchange = exchange;
}
@@ -104,7 +102,6 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
ApiClients.Add(apiClient);
return apiClient;
}
@@ -124,7 +121,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public virtual void Dispose()
{
_logger.Log(LogLevel.Debug, "Disposing client");
foreach (var client in ApiClients)
client.Dispose();
}
@@ -1,6 +1,7 @@
using System.Linq;
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
@@ -19,6 +20,9 @@ namespace CryptoExchange.Net.Clients
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger(name);
}
}
}
@@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
@@ -33,10 +35,13 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger(name);
}
/// <summary>
+104 -93
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
@@ -105,7 +106,7 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
RequestFactory.Configure(options, httpClient);
}
/// <summary>
@@ -238,13 +239,14 @@ 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]";
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
}
else
{
@@ -362,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 request = RequestFactory.Create(definition.Method, uri, requestId);
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, 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;
@@ -438,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)
@@ -455,17 +443,14 @@ namespace CryptoExchange.Net.Clients
{
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseLength = response.ContentLength;
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
accessor = CreateAccessor();
if (!response.IsSuccessStatusCode)
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{
// Error response
await accessor.Read(responseStream, true).ConfigureAwait(false);
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
@@ -481,29 +466,28 @@ namespace CryptoExchange.Net.Clients
}
else
{
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
}
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
}
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (!valid)
{
// Invalid json
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
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.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, 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)
@@ -516,31 +500,55 @@ namespace CryptoExchange.Net.Clients
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString();
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
}
else
{
// Request timed out
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
}
catch (ArgumentException argumentException)
{
if (argumentException.Message.StartsWith("Only HTTP/"))
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
catch (NotSupportedException notSupportedException)
{
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
finally
{
accessor?.Clear();
@@ -554,10 +562,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 +612,16 @@ namespace CryptoExchange.Net.Clients
{
if (contentType == Constants.JsonContentHeader)
{
var serializer = CreateSerializer();
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
// Write the parameters as json in the body
string stringData;
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(value);
stringData = stringSerializer.Serialize(value);
else
stringData = CreateSerializer().Serialize(parameters);
stringData = stringSerializer.Serialize(parameters);
request.SetContent(stringData, contentType);
}
else if (contentType == Constants.FormContentHeader)
@@ -625,11 +638,11 @@ namespace CryptoExchange.Net.Clients
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <param name="exception">Exception</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
return new ServerError(message);
return new ServerError(ErrorInfo.Unknown, exception);
}
/// <summary>
@@ -641,21 +654,19 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ServerRateLimitError(message);
return new ServerRateLimitError();
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError(message) { RetryAfter = datetime };
return new ServerRateLimitError() { RetryAfter = datetime };
return new ServerRateLimitError(message);
return new ServerRateLimitError();
}
/// <summary>
@@ -682,21 +693,21 @@ namespace CryptoExchange.Net.Clients
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
var localTime = DateTime.UtcNow;
@@ -725,7 +736,7 @@ namespace CryptoExchange.Net.Clients
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
private bool ShouldCache(RequestDefinition definition)
+87 -37
View File
@@ -1,8 +1,10 @@
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;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
@@ -41,6 +43,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Keep alive timeout for websocket connection
/// </summary>
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
/// </summary>
@@ -76,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
{
@@ -132,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
@@ -205,9 +217,9 @@ namespace CryptoExchange.Net.Clients
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
catch (OperationCanceledException tce)
{
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
}
try
@@ -215,7 +227,7 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
@@ -238,7 +250,7 @@ namespace CryptoExchange.Net.Clients
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(connectResult.Error!);
@@ -254,20 +266,21 @@ 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")));
}
subscription.Status = SubscriptionStatus.Subscribing;
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
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
if (!subResult)
{
waitEvent?.Set();
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Confirmed)
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
@@ -275,6 +288,7 @@ namespace CryptoExchange.Net.Clients
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
subscription.Status = SubscriptionStatus.Pending;
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
@@ -283,7 +297,7 @@ namespace CryptoExchange.Net.Clients
subscription.HandleSubQueryResponse(subQuery.Response!);
}
subscription.Confirmed = true;
subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
@@ -302,11 +316,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);
}
@@ -315,12 +328,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"));
@@ -333,7 +345,7 @@ namespace CryptoExchange.Net.Clients
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<THandlerResponse>(default);
@@ -346,7 +358,7 @@ namespace CryptoExchange.Net.Clients
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<THandlerResponse>(connectResult.Error!);
}
@@ -359,7 +371,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)
@@ -373,13 +385,14 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="socket">The connection to check</param>
/// <param name="authenticated">Whether the socket should authenticated</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
{
if (socket.Connected)
return CallResult.SuccessResult;
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
if (!connectResult)
return connectResult;
@@ -483,25 +496,56 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="ct">Cancellation token</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
{
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected);
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value)
.ToList();
SocketConnection connection;
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError());
}
break;
}
delayed = true;
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError());
}
if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList();
SocketConnection? connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
else
{
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
@@ -509,9 +553,12 @@ namespace CryptoExchange.Net.Clients
if (connection != null)
{
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(connection);
}
}
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
@@ -560,11 +607,12 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected async virtual Task HandleConnectRateLimitedAsync()
{
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
{
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
_logger.AddingRetryAfterGuard(retryAfter);
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
RateLimiter ??= new RateLimitGate("Connection");
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
}
}
@@ -572,10 +620,11 @@ namespace CryptoExchange.Net.Clients
/// Connect a socket
/// </summary>
/// <param name="socketConnection">The socket to connect</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
{
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult)
{
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
@@ -595,6 +644,7 @@ namespace CryptoExchange.Net.Clients
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
{
KeepAliveInterval = KeepAliveInterval,
KeepAliveTimeout = KeepAliveTimeout,
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
@@ -702,11 +752,11 @@ namespace CryptoExchange.Net.Clients
{
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult)
return socketResult.AsDataless();
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
if (!connectResult)
return new CallResult(connectResult.Error!);
}
@@ -799,9 +849,9 @@ namespace CryptoExchange.Net.Clients
cs.SubscriptionStates.ForEach(subState =>
{
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
});
}
});
@@ -0,0 +1,31 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Caching for JsonSerializerContext instances
/// </summary>
public static class JsonSerializerContextCache
{
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
/// <summary>
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
/// </summary>
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{
var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
var instance = new T();
_cache[contextType] = instance;
return instance;
}
}
}
@@ -8,6 +8,8 @@ using System.Text.Json;
using CryptoExchange.Net.Attributes;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Diagnostics;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
@@ -16,14 +18,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
#if NET5_0_OR_GREATER
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
# else
public class ArrayConverter<T, TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
#else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
{
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
@@ -39,11 +40,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStartArray();
var valueType = typeof(T);
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
typeAttributes = CacheTypeAttributes(valueType);
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
@@ -72,7 +69,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
TypeInfoResolver = (TContext)Activator.CreateInstance(typeof(TContext))!,
TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
@@ -101,77 +98,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = Activator.CreateInstance(typeof(T))!;
return (T)ParseObject(ref reader, result, typeof(T), options);
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static List<ArrayPropertyInfo> CacheTypeAttributes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
#else
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
#endif
{
var attributes = new List<ArrayPropertyInfo>();
var properties = type.GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
attributes.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
_typeAttributesCache.TryAdd(type, attributes);
return attributes;
var result = new T();
return ParseObject(ref reader, result, options);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options)
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#else
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
attributes = CacheTypeAttributes(objectType);
int index = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{
index++;
@@ -184,25 +133,23 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
object? value = null;
if (attribute.JsonConverter != null)
{
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
{
newOptions = new JsonSerializerOptions
if (attribute.JsonSerializerOptions == null)
{
attribute.JsonSerializerOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
_converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters((TContext)Activator.CreateInstance(typeof(TContext))!));
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
@@ -230,6 +177,50 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return result;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static List<ArrayPropertyInfo> CacheTypeAttributes()
#else
private static List<ArrayPropertyInfo> CacheTypeAttributes()
#endif
{
var attributes = new List<ArrayPropertyInfo>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
attributes.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
return attributes;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
@@ -237,6 +228,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
}
}
}
@@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.Json;
@@ -47,7 +48,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool");
return default;
}
@@ -21,7 +21,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return (reader.GetString()?.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? []);
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
}
/// <inheritdoc />
@@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -14,8 +15,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
@@ -39,17 +40,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable");
return default;
}
if (reader.TokenType is JsonTokenType.Number)
{
var longValue = reader.GetDouble();
if (longValue == 0 || longValue < 0)
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
return default;
return ParseFromDouble(longValue);
return ParseFromDecimal(decValue);
}
else if (reader.TokenType is JsonTokenType.String)
{
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| stringValue == "0001-01-01T00:00:00Z"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
{
return default;
}
@@ -88,20 +89,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
/// <summary>
/// Parse a long value to datetime
/// Parse a double value to datetime
/// </summary>
/// <param name="longValue"></param>
/// <returns></returns>
public static DateTime ParseFromDouble(double longValue)
{
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
public static DateTime ParseFromDouble(double value)
=> ParseFromDecimal((decimal)value);
return ConvertFromNanoseconds(longValue);
/// <summary>
/// Parse a decimal value to datetime
/// </summary>
public static DateTime ParseFromDecimal(decimal value)
{
if (value < 19999999999)
return ConvertFromSeconds(value);
if (value < 19999999999999)
return ConvertFromMilliseconds(value);
if (value < 19999999999999999)
return ConvertFromMicroseconds(value);
return ConvertFromNanoseconds(value);
}
/// <summary>
@@ -120,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
@@ -133,7 +138,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
@@ -146,25 +151,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
{
// Parse 1637745563.000 format
if (doubleValue <= 0)
if (decimalValue <= 0)
return default;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue);
if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue);
if (decimalValue < 19999999999)
return ConvertFromSeconds(decimalValue);
if (decimalValue < 19999999999999)
return ConvertFromMilliseconds(decimalValue);
if (decimalValue < 19999999999999999)
return ConvertFromMicroseconds(decimalValue);
return ConvertFromNanoseconds((long)doubleValue);
return ConvertFromNanoseconds(decimalValue);
}
if (stringValue.Length == 10)
@@ -175,7 +180,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
return default;
}
@@ -188,54 +193,70 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="nanoseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
}
@@ -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
@@ -1,4 +1,6 @@
using CryptoExchange.Net.Attributes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -67,6 +69,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static List<KeyValuePair<T, string>>? _mapping = null;
private NullableEnumConverter? _nullableEnumConverter = null;
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
internal class NullableEnumConverter : JsonConverter<T?>
{
private readonly EnumConverter<T> _enumConverter;
@@ -99,15 +103,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
if (t == null)
{
if (isEmptyString)
if (isEmptyString && !_unknownValuesWarned.Contains(null))
{
// We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
}
else
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
}
return new T(); // return default value
}
else
@@ -126,17 +127,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
JsonTokenType.Number => reader.GetInt32().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (string.IsNullOrEmpty(stringValue))
if (stringValue is null)
return null;
if (!GetValue(enumType, stringValue!, out var result))
if (!GetValue(enumType, stringValue, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{
@@ -145,7 +146,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
else
{
// We received an enum value but weren't able to parse it.
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
if (!_unknownValuesWarned.Contains(stringValue))
{
_unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
}
}
return null;
@@ -184,6 +189,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return true;
}
if (_unknownValuesWarned.Contains(value))
{
// Check if it is an known unknown value
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
result = default;
return false;
}
if (String.IsNullOrEmpty(value))
{
// An empty/null value will always fail when parsing, so just return here
result = default;
return false;
}
try
{
// If no explicit mapping is found try to parse string
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary>
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
/// </summary>
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver)
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
{
if (!_cache.TryGetValue(typeResolver, out var options))
{
@@ -33,6 +33,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
},
TypeInfoResolver = typeResolver,
};
foreach (var converter in additionalConverters)
options.Converters.Add(converter);
options.TypeInfoResolver = typeResolver;
_cache.TryAdd(typeResolver, options);
}
@@ -0,0 +1,60 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
var result = new T();
result.QuantityInBaseAsset = baseQuantity;
result.QuantityInQuoteAsset = quoteQuantity;
result.QuantityInContracts = contractQuantity;
return result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
if (value.QuantityInBaseAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
if (value.QuantityInQuoteAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
if (value.QuantityInContracts == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInContracts.Value);
writer.WriteEndArray();
}
}
}
@@ -0,0 +1,46 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
{
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
reader.Read();
var baseAsset = reader.GetString()!;
reader.Read();
var quoteAsset = reader.GetString()!;
reader.Read();
var timeStr = reader.GetString()!;
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
}
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteStringValue(value.TradingMode.ToString());
writer.WriteStringValue(value.BaseAsset);
writer.WriteStringValue(value.QuoteAsset);
writer.WriteStringValue(value.DeliverTime?.ToString());
writer.WriteEndArray();
}
}
}
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsJson { get; set; }
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
if (!IsValid)
return new CallResult<object>(GetOriginalString());
if (_document == null)
@@ -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}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
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, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
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}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
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, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
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);
@@ -171,9 +169,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public List<T?>? GetValues<T>(MessagePath path)
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
@@ -183,12 +181,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<List<T>>(_customSerializerOptions)!;
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
}
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsJson)
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
@@ -279,14 +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 ServerError("JsonError: " + ex.Message));
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 ServerError("JsonError: " + ex.Message));
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;
+13 -18
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
@@ -6,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>8.8.0</PackageVersion>
<AssemblyVersion>8.8.0</AssemblyVersion>
<FileVersion>8.8.0</FileVersion>
<PackageVersion>9.12.0</PackageVersion>
<AssemblyVersion>9.12.0</AssemblyVersion>
<FileVersion>9.12.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>
@@ -27,8 +27,8 @@
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="AOT" Condition=" '$(TargetFramework)' == 'NET8_0' Or '$(TargetFramework)' == 'NET9_0' ">
<IsAotCompatible>true</IsAotCompatible>
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -37,12 +37,6 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup>
@@ -57,10 +51,11 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="System.Text.Json" Version="9.0.10" />
</ItemGroup>
<ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
</ItemGroup>
</Project>
+72 -2
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;
@@ -87,8 +88,6 @@ namespace CryptoExchange.Net
value -= offset;
else value += (step.Value - offset);
}
value = RoundDown(value, 8);
return value.Normalize();
}
@@ -112,6 +111,34 @@ namespace CryptoExchange.Net
return RoundToSignificantDigits(value, precision.Value, roundingType);
}
/// <summary>
/// Apply the provided rules to the value
/// </summary>
/// <param name="value">Value to be adjusted</param>
/// <param name="decimals">Max decimal places</param>
/// <param name="valueStep">The value step for increase/decrease value</param>
/// <returns></returns>
public static decimal ApplyRules(
decimal value,
int? decimals = null,
decimal? valueStep = null)
{
if (valueStep.HasValue)
{
var offset = value % valueStep.Value;
if (offset != 0)
{
if (offset < valueStep.Value / 2)
value -= offset;
else value += (valueStep.Value - offset);
}
}
if (decimals.HasValue)
value = Math.Round(value, decimals.Value);
return value;
}
/// <summary>
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
/// </summary>
@@ -313,5 +340,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, x.QuoteAsset, (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, x.QuoteAsset, (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
}
/// <summary>
+47 -3
View File
@@ -399,7 +399,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Whether the trading mode is linear
/// </summary>
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
/// <summary>
/// Whether the trading mode is inverse
@@ -416,6 +416,36 @@ namespace CryptoExchange.Net
/// </summary>
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
/// <summary>
/// Whether the account type is a futures account
/// </summary>
public static bool IsFuturesAccount(this SharedAccountType type) =>
type == SharedAccountType.PerpetualLinearFutures
|| type == SharedAccountType.DeliveryLinearFutures
|| type == SharedAccountType.PerpetualInverseFutures
|| type == SharedAccountType.DeliveryInverseFutures;
/// <summary>
/// Whether the account type is a margin account
/// </summary>
public static bool IsMarginAccount(this SharedAccountType type) =>
type == SharedAccountType.CrossMargin
|| type == SharedAccountType.IsolatedMargin;
/// <summary>
/// Map a TradingMode value to a SharedAccountType enum value
/// </summary>
public static SharedAccountType ToAccountType(this TradingMode mode)
{
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
}
/// <summary>
/// Register rest client interfaces
/// </summary>
@@ -443,6 +473,10 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITransferRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
@@ -450,6 +484,10 @@ namespace CryptoExchange.Net
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
@@ -471,6 +509,12 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
return services;
}
@@ -486,8 +530,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>
@@ -52,19 +53,27 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
List<T?>? GetValues<T>(MessagePath path);
T?[]? GetValues<T>(MessagePath path);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<object> Deserialize(Type type, MessagePath? path = null);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<T> Deserialize<T>(MessagePath? path = null);
/// <summary>
@@ -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
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
Uri Uri { get; }
/// <summary>
/// HTTP protocol version
/// </summary>
Version HttpVersion { get; }
/// <summary>
/// internal request id for tracing
/// </summary>
int RequestId { get; }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System;
using System.Net.Http;
@@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Create a request for an uri
/// </summary>
/// <param name="method"></param>
/// <param name="uri"></param>
/// <param name="requestId"></param>
/// <returns></returns>
IRequest Create(HttpMethod method, Uri uri, int requestId);
IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId);
/// <summary>
/// Configure the requests created by this factory
/// </summary>
/// <param name="requestTimeout">Request timeout to use</param>
/// <param name="options">Rest client options</param>
/// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
void Configure(RestExchangeOptions options, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
/// <param name="httpKeepAliveInterval">Http client keep alive interval</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval);
}
}
+7 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
HttpStatusCode StatusCode { get; }
/// <summary>
/// Http protocol version
/// </summary>
Version HttpVersion { get; }
/// <summary>
/// Whether the status code indicates a success status
/// </summary>
@@ -0,0 +1,45 @@
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.Klines;
using CryptoExchange.Net.Trackers.Trades;
using System;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Tracker factory
/// </summary>
public interface ITrackerFactory
{
/// <summary>
/// Whether the factory supports creating a KlineTracker instance for this symbol and interval
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="interval">The kline interval</param>
bool CanCreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval);
/// <summary>
/// Create a new kline tracker
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="interval">Kline interval</param>
/// <param name="limit">The max amount of klines to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <returns></returns>
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
/// <summary>
/// Whether the factory supports creating a TradeTracker instance for this symbol
/// </summary>
/// <param name="symbol">The symbol</param>
bool CanCreateTradeTracker(SharedSymbol symbol);
/// <summary>
/// Create a new trade tracker for a symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="limit">The max amount of trades to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <returns></returns>
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
}
}
+10 -2
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@@ -75,15 +76,22 @@ namespace CryptoExchange.Net.Interfaces
/// Connect the socket
/// </summary>
/// <returns></returns>
Task<CallResult> ConnectAsync();
Task<CallResult> ConnectAsync(CancellationToken ct);
/// <summary>
/// Send data
/// Send string data
/// </summary>
/// <param name="id"></param>
/// <param name="data"></param>
/// <param name="weight"></param>
bool Send(int id, string data, int weight);
/// <summary>
/// Send byte data
/// </summary>
/// <param name="id"></param>
/// <param name="data"></param>
/// <param name="weight"></param>
bool Send(int id, byte[] data, int weight);
/// <summary>
/// Reconnect the socket
/// </summary>
/// <returns></returns>
+113 -1
View File
@@ -1,5 +1,9 @@
using System;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net
@@ -9,11 +13,50 @@ namespace CryptoExchange.Net
/// </summary>
public static class LibraryHelpers
{
private static ILogger? _staticLogger;
/// <summary>
/// Static logger
/// </summary>
public static ILogger? StaticLogger
{
get => _staticLogger;
internal set
{
if (_staticLogger != null)
return;
_staticLogger = value;
}
}
/// <summary>
/// Client order id separator
/// </summary>
public const string ClientOrderIdSeparator = "JK";
private static Dictionary<string, string> _defaultClientReferences = new Dictionary<string, string>()
{
{ "Binance.Spot", "x-VICEW9VV" },
{ "Binance.Futures", "x-d63tKbx3" },
{ "BingX", "easytrading" },
{ "Bitfinex", "kCCe-CNBO" },
{ "Bitget", "6x21p" },
{ "BitMart", "EASYTRADING0001" },
{ "BitMEX", "Sent from JKorf" },
{ "BloFin", "5c07cf695885c282" },
{ "Bybit", "Zx000356" },
{ "CoinEx", "x-147866029-" },
{ "GateIo", "copytraderpw" },
{ "HTX", "AA1ef14811" },
{ "Kucoin.FuturesName", "Easytradingfutures" },
{ "Kucoin.FuturesKey", "9e08c05f-454d-4580-82af-2f4c7027fd00" },
{ "Kucoin.SpotName", "Easytrading" },
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
{ "Mexc", "EASYT" },
{ "OKX", "1425d83a94fbBCDE" },
{ "XT", "4XWeqN10M1fcoI5L" },
};
/// <summary>
/// Apply broker id to a client order id
/// </summary>
@@ -43,5 +86,74 @@ namespace CryptoExchange.Net
return clientOrderId;
}
/// <summary>
/// Get the client reference for an exchange if available
/// </summary>
public static string GetClientReference(Func<string?> optionsReference, string exchange, string? topic = null)
{
var optionsValue = optionsReference();
if (!string.IsNullOrEmpty(optionsValue))
return optionsValue!;
var key = exchange;
if (topic != null)
key += "." + topic;
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
}
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
{
#if NET5_0_OR_GREATER
var socketHandler = new SocketsHttpHandler();
try
{
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
{
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
}
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
socketHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
return socketHandler;
#else
var httpHandler = new HttpClientHandler();
try
{
httpHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
httpHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
httpHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
return httpHandler;
#endif
}
}
}
@@ -8,6 +8,7 @@ namespace CryptoExchange.Net.Logging.Extensions
{
private static readonly Action<ILogger, int, Exception?> _connecting;
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
private static readonly Action<ILogger, int, Exception?> _connectingCanceled;
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
@@ -189,6 +190,12 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(1030, "SocketPingTimeout"),
"[Sckt {Id}] ping frame timeout; reconnecting socket");
_connectingCanceled = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1031, "ConnectingCanceled"),
"[Sckt {SocketId}] connecting canceled");
}
public static void SocketConnecting(
@@ -370,5 +377,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_socketPingTimeout(logger, socketId, null);
}
public static void SocketConnectingCanceled(
this ILogger logger, int socketId)
{
_connectingCanceled(logger, socketId, null);
}
}
}
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class RestApiClientLoggingExtensions
{
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, string?, Exception?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
@@ -25,10 +25,10 @@ namespace CryptoExchange.Net.Logging.Extensions
static RestApiClientLoggingExtensions()
{
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?>(
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
LogLevel.Warning,
new EventId(4000, "RestApiErrorReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}");
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
LogLevel.Debug,
@@ -92,9 +92,9 @@ namespace CryptoExchange.Net.Logging.Extensions
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
{
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, null);
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, originalData, exception);
}
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
private static readonly Action<ILogger, Exception?> _timeoutWaitingForReconnectingSocket;
private static readonly Action<ILogger, long, Exception?> _waitedForReconnectingSocket;
static SocketApiClientLoggingExtension()
{
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
"Timeout while waiting for existing socket reconnection, failing request");
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
LogLevel.Trace,
new EventId(3020, "WaitedForReconnectingSocket"),
"Waited for reconnecting socket for {Timespan}ms");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_addingRetryAfterGuard(logger, retryAfter, null);
}
public static void TimeoutWaitingForReconnectingSocket(this ILogger logger)
{
_timeoutWaitingForReconnectingSocket(logger, null);
}
public static void WaitedForReconnectingSocket(this ILogger logger, long milliseconds)
{
_waitedForReconnectingSocket(logger, milliseconds, null);
}
}
}
@@ -13,11 +13,11 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _unknownExceptionWhileProcessingReconnection;
private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails;
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 +37,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()
{
@@ -70,11 +71,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2005, "WebSocketError"),
"[Sckt {SocketId}] error: {ErrorMessage}");
_messageSentNotPending = LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(2006, "MessageSentNotPending"),
"[Sckt {SocketId}] [Req {RequestId}] message sent, but not pending");
_receivedData = LoggerMessage.Define<int, string>(
LogLevel.Trace,
new EventId(2007, "ReceivedData"),
@@ -90,11 +86,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 +179,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)
@@ -221,15 +228,16 @@ namespace CryptoExchange.Net.Logging.Extensions
_webSocketError(logger, socketId, errorMessage, e);
}
public static void MessageSentNotPending(this ILogger logger, int socketId, int requestId)
{
_messageSentNotPending(logger, socketId, requestId, null);
}
public static void ReceivedData(this ILogger logger, int socketId, string originalData)
{
_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,17 +246,17 @@ 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)
{
_receivedMessageNotRecognized(logger, socketId, id, null);
}
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage)
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex)
{
_failedToDeserializeMessage(logger, socketId, errorMessage, null);
_failedToDeserializeMessage(logger, socketId, errorMessage, ex);
}
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
{
@@ -321,5 +329,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
{
_sendingByteData(logger, socketId, requestId, length, null);
}
}
}
@@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"{Api} order book {Symbol} connection lost");
_orderBookDisconnected = LoggerMessage.Define<string, string>(
LogLevel.Warning,
LogLevel.Debug,
new EventId(5004, "OrderBookDisconnected"),
"{Api} order book {Symbol} disconnected");
@@ -173,9 +173,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_klineTrackerStarting(logger, symbol, null);
}
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception)
{
_klineTrackerStartFailed(logger, symbol, error, null);
_klineTrackerStartFailed(logger, symbol, error, exception);
}
public static void KlineTrackerStarted(this ILogger logger, string symbol)
@@ -233,9 +233,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_tradeTrackerStarting(logger, symbol, null);
}
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex)
{
_tradeTrackerStartFailed(logger, symbol, error, null);
_tradeTrackerStartFailed(logger, symbol, error, ex);
}
public static void TradeTrackerStarted(this ILogger logger, string symbol)
+50
View File
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// An alias used by the exchange for an asset commonly known by another name
/// </summary>
public class AssetAlias
{
/// <summary>
/// Alias type
/// </summary>
public AliasType Type { get; set; }
/// <summary>
/// The name of the asset on the exchange
/// </summary>
public string ExchangeAssetName { get; set; }
/// <summary>
/// The name of the asset as it's commonly known
/// </summary>
public string CommonAssetName { get; set; }
/// <summary>
/// ctor
/// </summary>
public AssetAlias(string exchangeName, string commonName, AliasType type = AliasType.BothWays)
{
ExchangeAssetName = exchangeName;
CommonAssetName = commonName;
Type = type;
}
}
/// <summary>
/// Alias type
/// </summary>
public enum AliasType
{
/// <summary>
/// Translate both from and to exchange
/// </summary>
BothWays,
/// <summary>
/// Only translate when converting to exchange
/// </summary>
OnlyToExchange
}
}
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Exchange configuration for asset aliases
/// </summary>
public class AssetAliasConfiguration
{
/// <summary>
/// Defined aliases
/// </summary>
public AssetAlias[] Aliases { get; set; } = [];
/// <summary>
/// Auto convert asset names when using the Shared interfaces. Defaults to true
/// </summary>
public bool AutoConvertEnabled { get; set; } = true;
/// <summary>
/// Map the common name to an exchange name for an asset. If there is no alias the input name is returned
/// </summary>
public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.FirstOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName;
/// <summary>
/// Map the exchange name to a common name for an asset. If there is no alias the input name is returned
/// </summary>
public string ExchangeToCommonName(string exchangeName)
{
if (!AutoConvertEnabled)
return exchangeName;
var alias = Aliases.FirstOrDefault(x => x.ExchangeAssetName == exchangeName);
if (alias == null || alias.Type == AliasType.OnlyToExchange)
return exchangeName;
return alias.CommonAssetName;
}
}
}
+55 -32
View File
@@ -166,6 +166,18 @@ namespace CryptoExchange.Net.Objects
return new CallResult(error);
}
/// <summary>
/// Copy the CallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public CallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new CallResult<K>(data, OriginalData, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
@@ -193,6 +205,11 @@ namespace CryptoExchange.Net.Objects
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
@@ -214,6 +231,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; internal set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
@@ -232,19 +254,12 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime,
string? originalData,
int? requestId,
string? requestUrl,
string? requestBody,
@@ -253,9 +268,11 @@ namespace CryptoExchange.Net.Objects
Error? error) : base(error)
{
ResponseStatusCode = code;
HttpVersion = httpVersion;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestId = requestId;
OriginalData = originalData;
RequestUrl = requestUrl;
RequestBody = requestBody;
@@ -276,7 +293,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult AsError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
@@ -287,7 +304,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
}
/// <summary>
@@ -324,7 +341,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
}
/// <inheritdoc />
@@ -345,6 +362,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
@@ -393,21 +415,9 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// Create a new result
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="responseLength"></param>
/// <param name="originalData"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="dataSource"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
@@ -421,6 +431,7 @@ namespace CryptoExchange.Net.Objects
[AllowNull] T data,
Error? error) : base(data, originalData, error)
{
HttpVersion = httpVersion;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
@@ -440,7 +451,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult AsDataless()
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
@@ -448,14 +459,14 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
@@ -465,7 +476,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
}
/// <summary>
@@ -476,7 +487,19 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
}
/// <summary>
@@ -547,7 +570,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
internal WebCallResult<T> Cached()
{
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
}
/// <inheritdoc />
@@ -558,7 +581,7 @@ namespace CryptoExchange.Net.Objects
if (ResponseLength != null)
sb.Append($", {ResponseLength} bytes");
if (ResponseTime != null)
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
return sb.ToString();
}
+43
View File
@@ -251,4 +251,47 @@ 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
}
/// <summary>
/// Subscription status
/// </summary>
public enum SubscriptionStatus
{
/// <summary>
/// Pending, waiting before (re)subscription can be started
/// </summary>
Pending,
/// <summary>
/// Currently (re)subscribing, will start producing updates soon if subscription is successful
/// </summary>
Subscribing,
/// <summary>
/// Subscribed and listening to updates
/// </summary>
Subscribed,
/// <summary>
/// Subscription is being closed and will stop producing updates
/// </summary>
Closing,
/// <summary>
/// Subscription is closed and will no long produce updates
/// </summary>
Closed
}
}
+147 -117
View File
@@ -1,4 +1,5 @@
using System;
using CryptoExchange.Net.Objects.Errors;
using System;
namespace CryptoExchange.Net.Objects
{
@@ -7,32 +8,69 @@ 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>
/// The data which caused the error
/// Error type
/// </summary>
public object? Data { get; set; }
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
/// </summary>
public Exception? Exception { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected Error(int? code, string message, object? data)
protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception)
{
Code = code;
Message = message;
Data = data;
ErrorCode = errorCode;
ErrorType = errorInfo.ErrorType;
Message = errorInfo.Message;
ErrorDescription = errorInfo.ErrorDescription;
IsTransient = errorInfo.IsTransient;
Exception = exception;
}
/// <summary>
@@ -41,7 +79,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public override string ToString()
{
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}";
}
}
@@ -51,17 +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>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
public CantConnectError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -70,17 +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>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
public NoApiCredentialsError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -91,25 +138,19 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
/// <param name="data"></param>
public ServerError(string message, object? data = null) : base(null, message, data) { }
public ServerError(ErrorInfo errorInfo, Exception? exception = null)
: base(null, errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null)
: this(errorCode.ToString(), errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { }
}
/// <summary>
@@ -118,27 +159,30 @@ namespace CryptoExchange.Net.Objects
public class WebError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
/// <param name="message"></param>
/// <param name="data"></param>
public WebError(string message, object? data = null) : base(null, message, data) { }
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>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
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>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected WebError(int? code, string message, object? data): base(code, message, data) { }
public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
@@ -147,40 +191,14 @@ namespace CryptoExchange.Net.Objects
public class DeserializeError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
/// <param name="message">The error message</param>
/// <param name="data">The data which caused the error</param>
public DeserializeError(string message, object? data) : base(null, message, data) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data");
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
/// Unknown error
/// </summary>
public class UnknownError : Error
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message">Error message</param>
/// <param name="data">Error data</param>
public UnknownError(string message, object? data = null) : base(null, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
@@ -189,18 +207,28 @@ namespace CryptoExchange.Net.Objects
public class ArgumentError : Error
{
/// <summary>
/// ctor
/// Default error info for missing parameter
/// </summary>
/// <param name="message"></param>
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>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
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>
@@ -216,10 +244,7 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
}
/// <summary>
@@ -228,18 +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>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
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>
@@ -248,18 +274,19 @@ namespace CryptoExchange.Net.Objects
public class ServerRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
/// <param name="message"></param>
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded");
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
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>
@@ -268,17 +295,19 @@ namespace CryptoExchange.Net.Objects
public class CancellationRequestedError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
}
/// <summary>
@@ -287,17 +316,18 @@ namespace CryptoExchange.Net.Objects
public class InvalidOperationError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
/// <param name="message"></param>
public InvalidOperationError(string message) : base(null, message, null) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid");
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
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
}
}
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
{
targetOptions.ApiCredentials = ApiCredentials;
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
targetOptions.Environment = Environment;
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
targetOptions.Rest = Rest.Set(targetOptions.Rest);
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Net;
using System.Net.Http;
namespace CryptoExchange.Net.Objects.Options
{
@@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// The HTTP protocol version to use, typically 2.0 or 1.1
/// </summary>
public Version HttpVersion { get; set; }
#if NET5_0_OR_GREATER
= new Version(2, 0);
#else
= new Version(1, 1);
#endif
/// <summary>
/// Http client keep alive interval for keeping connections open
/// </summary>
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
/// <summary>
/// Set the values of this options on the target options
/// </summary>
@@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge;
item.HttpVersion = HttpVersion;
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
return item;
}
}
@@ -94,7 +94,7 @@ namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public TEnvironment Environment { get; set; }
@@ -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;
}
}
}
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects.Sockets
@@ -12,13 +14,27 @@ namespace CryptoExchange.Net.Objects.Sockets
private readonly SocketConnection _connection;
private readonly Subscription _listener;
private object _eventLock = new object();
private bool _connectionEventsSubscribed = true;
private List<Action> _connectionClosedEventHandlers = new List<Action>();
private List<Action> _connectionLostEventHandlers = new List<Action>();
private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>();
private List<Action<TimeSpan>> _connectionRestoredEventHandlers = new List<Action<TimeSpan>>();
private List<Action> _activityPausedEventHandlers = new List<Action>();
private List<Action> _activityUnpausedEventHandlers = new List<Action>();
/// <summary>
/// Event when the status of the subscription changes
/// </summary>
public event Action<SubscriptionStatus>? SubscriptionStatusChanged;
/// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible.
/// </summary>
public event Action ConnectionLost
{
add => _connection.ConnectionLost += value;
remove => _connection.ConnectionLost -= value;
add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
}
/// <summary>
@@ -26,8 +42,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action ConnectionClosed
{
add => _connection.ConnectionClosed += value;
remove => _connection.ConnectionClosed -= value;
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
}
/// <summary>
@@ -35,8 +51,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action<Error> ResubscribingFailed
{
add => _connection.ResubscribingFailed += value;
remove => _connection.ResubscribingFailed -= value;
add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
}
/// <summary>
@@ -46,8 +62,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action<TimeSpan> ConnectionRestored
{
add => _connection.ConnectionRestored += value;
remove => _connection.ConnectionRestored -= value;
add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
}
/// <summary>
@@ -55,8 +71,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action ActivityPaused
{
add => _connection.ActivityPaused += value;
remove => _connection.ActivityPaused -= value;
add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
}
/// <summary>
@@ -64,8 +80,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action ActivityUnpaused
{
add => _connection.ActivityUnpaused += value;
remove => _connection.ActivityUnpaused -= value;
add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
}
/// <summary>
@@ -95,7 +111,128 @@ namespace CryptoExchange.Net.Objects.Sockets
public UpdateSubscription(SocketConnection connection, Subscription subscription)
{
_connection = connection;
_connection.ConnectionClosed += HandleConnectionClosedEvent;
_connection.ConnectionLost += HandleConnectionLostEvent;
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
_connection.ActivityPaused += HandlePausedEvent;
_connection.ActivityUnpaused += HandleUnpausedEvent;
_listener = subscription;
_listener.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
private void UnsubscribeConnectionEvents()
{
lock (_eventLock)
{
if (!_connectionEventsSubscribed)
return;
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
_connectionEventsSubscribed = false;
}
}
private void HandleConnectionClosedEvent()
{
UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting
if (!_listener.IsClosingConnection)
return;
List<Action> handlers;
lock (_eventLock)
handlers = _connectionClosedEventHandlers.ToList();
foreach(var callback in handlers)
callback();
}
private void HandleConnectionLostEvent()
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers;
lock (_eventLock)
handlers = _connectionLostEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action<TimeSpan>> handlers;
lock (_eventLock)
handlers = _connectionRestoredEventHandlers.ToList();
foreach (var callback in handlers)
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action<Error>> handlers;
lock (_eventLock)
handlers = _resubscribeFailedEventHandlers.ToList();
foreach (var callback in handlers)
callback(error);
}
private void HandlePausedEvent()
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers;
lock (_eventLock)
handlers = _activityPausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleUnpausedEvent()
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers;
lock (_eventLock)
handlers = _activityUnpausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
/// <summary>
@@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public TimeSpan? KeepAliveInterval { get; set; }
/// <summary>
/// Timeout for keep alive response messages
/// </summary>
public TimeSpan? KeepAliveTimeout { get; set; }
/// <summary>
/// The rate limiter for the socket connection
/// </summary>
+1 -1
View File
@@ -56,7 +56,7 @@ namespace CryptoExchange.Net.Objects
if (!IsEnabled(logLevel))
return;
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}{(exception == null ? string.Empty : (", " + exception.ToLogString()))}";
Trace.WriteLine(logMessage);
}
}
@@ -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
{
@@ -46,11 +46,11 @@ namespace CryptoExchange.Net.RateLimiting
{
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (TaskCanceledException tce)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
return new CallResult(new CancellationRequestedError(tce));
}
finally
{
@@ -81,11 +81,11 @@ namespace CryptoExchange.Net.RateLimiting
{
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (TaskCanceledException tce)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
return new CallResult(new CancellationRequestedError(tce));
}
finally
{
+6 -1
View File
@@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public Uri Uri => _request.RequestUri!;
/// <inheritdoc />
public Version HttpVersion => _request.Version!;
/// <inheritdoc />
public int RequestId { get; }
@@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
{
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
return new Response(response);
}
}
}
+16 -26
View File
@@ -3,6 +3,7 @@ using System.Net;
using System.Net.Http;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Requests
{
@@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests
private HttpClient? _httpClient;
/// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
public void Configure(RestExchangeOptions options, HttpClient? client = null)
{
if (client == null)
client = CreateClient(proxy, requestTimeout);
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
_httpClient = client;
}
/// <inheritdoc />
public IRequest Create(HttpMethod method, Uri uri, int requestId)
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
{
if (_httpClient == null)
throw new InvalidOperationException("Cant create request before configuring http client");
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Version = httpRequestVersion;
#if NET5_0_OR_GREATER
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
#endif
return new Request(requestMessage, _httpClient, requestId);
}
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{
_httpClient = CreateClient(proxy, requestTimeout);
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
var client = new HttpClient(handler)
{
Timeout = requestTimeout
Timeout = requestTimeout
};
return client;
}
}
}
+5 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public HttpStatusCode StatusCode => _response.StatusCode;
/// <inheritdoc />
public Version HttpVersion => _response.Version;
/// <inheritdoc />
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Account type
/// </summary>
public enum SharedAccountType
{
/// <summary>
/// Unified account, combined account for multiple different types of trading
/// </summary>
Unified,
/// <summary>
/// Funding account, where withdrawals and deposits are made from and to
/// </summary>
Funding,
/// <summary>
/// Spot trading account
/// </summary>
Spot,
/// <summary>
/// Cross margin account
/// </summary>
CrossMargin,
/// <summary>
/// Isolated margin account
/// </summary>
IsolatedMargin,
/// <summary>
/// Perpetual linear futures account
/// </summary>
PerpetualLinearFutures,
/// <summary>
/// Delivery linear futures account
/// </summary>
DeliveryLinearFutures,
/// <summary>
/// Perpetual inverse futures account
/// </summary>
PerpetualInverseFutures,
/// <summary>
/// Delivery inverse futures account
/// </summary>
DeliveryInverseFutures,
/// <summary>
/// Option account
/// </summary>
Option,
/// <summary>
/// Other
/// </summary>
Other
}
}
@@ -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
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Take Profit / Stop Loss side
/// </summary>
public enum SharedTpSlSide
{
/// <summary>
/// Take profit
/// </summary>
TakeProfit,
/// <summary>
/// Stop loss
/// </summary>
StopLoss
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// The order direction when order trigger parameters are reached
/// </summary>
public enum SharedTriggerOrderDirection
{
/// <summary>
/// Enter, Buy for Spot and long futures positions, Sell for short futures positions
/// </summary>
Enter,
/// <summary>
/// Exit, Sell for Spot and long futures positions, Buy for short futures positions
/// </summary>
Exit
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Trigger order status
/// </summary>
public enum SharedTriggerOrderStatus
{
/// <summary>
/// Order is active
/// </summary>
Active,
/// <summary>
/// Order has been filled
/// </summary>
Filled,
/// <summary>
/// Trigger canceled, can be user cancelation or system cancelation due to an error
/// </summary>
CanceledOrRejected,
/// <summary>
/// Trigger order has been triggered. Resulting order might be filled or not.
/// </summary>
Triggered
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Price direction for trigger order
/// </summary>
public enum SharedTriggerPriceDirection
{
/// <summary>
/// Trigger when the price goes below the specified trigger price
/// </summary>
PriceBelow,
/// <summary>
/// Trigger when the price goes above the specified trigger price
/// </summary>
PriceAbove
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Price direction for trigger order
/// </summary>
public enum SharedTriggerPriceType
{
/// <summary>
/// Last traded price
/// </summary>
LastPrice,
/// <summary>
/// Mark price
/// </summary>
MarkPrice,
/// <summary>
/// Index price
/// </summary>
IndexPrice
}
}
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Client for managing futures orders using a client order id
/// </summary>
public interface IFuturesOrderClientIdClient : ISharedClient
public interface IFuturesOrderClientIdRestClient : ISharedClient
{
/// <summary>
/// Futures get order by client order id request options
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Take profit / Stop loss client
/// </summary>
public interface IFuturesTpSlRestClient : ISharedClient
{
/// <summary>
/// Set take profit and/or stop loss options
/// </summary>
EndpointOptions<SetTpSlRequest> SetFuturesTpSlOptions { get; }
/// <summary>
/// Set a take profit and/or stop loss for an open position
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedId>> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default);
/// <summary>
/// Cancel a take profit and/or stop loss options
/// </summary>
EndpointOptions<CancelTpSlRequest> CancelFuturesTpSlOptions { get; }
/// <summary>
/// Cancel an active take profit and/or stop loss for an open position
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<bool>> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default);
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Client for placing trigger orders
/// </summary>
public interface IFuturesTriggerOrderRestClient : ISharedClient
{
/// <summary>
/// Place spot trigger order options
/// </summary>
PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; }
/// <summary>
/// Place a new trigger order
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedId>> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Get trigger order request options
/// </summary>
EndpointOptions<GetOrderRequest> GetFuturesTriggerOrderOptions { get; }
/// <summary>
/// Get info on a specific trigger order
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Cancel trigger order request options
/// </summary>
EndpointOptions<CancelOrderRequest> CancelFuturesTriggerOrderOptions { get; }
/// <summary>
/// Cancel a trigger order
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
}
}
@@ -12,7 +12,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Balances request options
/// </summary>
EndpointOptions<GetBalancesRequest> GetBalancesOptions { get; }
GetBalancesOptions GetBalancesOptions { get; }
/// <summary>
/// Get balances for the user
@@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Client for retrieving the current best bid/ask price
/// </summary>
public interface IBookTickerRestClient : ISharedClient
{
/// <summary>
/// Book ticker request options
/// </summary>
EndpointOptions<GetBookTickerRequest> GetBookTickerOptions { get; }
/// <summary>
/// Get the best ask/bid info for a symbol
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
}
}
@@ -0,0 +1,25 @@
using CryptoExchange.Net.Objects;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Client for transferring funds between account types
/// </summary>
public interface ITransferRestClient : ISharedClient
{
/// <summary>
/// Transfer request options
/// </summary>
TransferOptions TransferOptions { get; }
/// <summary>
/// Transfer funds between account types
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> TransferAsync(TransferRequest request, CancellationToken ct = default);
}
}
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Client for managing spot orders using a client order id
/// </summary>
public interface ISpotOrderClientIdClient : ISharedClient
public interface ISpotOrderClientIdRestClient : ISharedClient
{
/// <summary>
/// Spot get order by client order id request options
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Client for placing trigger orders
/// </summary>
public interface ISpotTriggerOrderRestClient : ISharedClient
{
/// <summary>
/// Place spot trigger order options
/// </summary>
PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; }
/// <summary>
/// Place a new trigger order
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedId>> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Get trigger order request options
/// </summary>
EndpointOptions<GetOrderRequest> GetSpotTriggerOrderOptions { get; }
/// <summary>
/// Get info on a specific trigger order
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedSpotTriggerOrder>> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Cancel trigger order request options
/// </summary>
EndpointOptions<CancelOrderRequest> CancelSpotTriggerOrderOptions { get; }
/// <summary>
/// Cancel a trigger order
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
}
}
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis
WebCallResult<T> result,
INextPageToken? nextPageToken = null) :
base(result.ResponseStatusCode,
result.HttpVersion,
result.ResponseHeaders,
result.ResponseTime,
result.ResponseLength,
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis
WebCallResult<T> result,
INextPageToken? nextPageToken = null) :
base(result.ResponseStatusCode,
result.HttpVersion,
result.ResponseHeaders,
result.ResponseTime,
result.ResponseLength,
@@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis
string exchange,
TradingMode[]? dataTradeModes,
HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
@@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis
Error? error,
INextPageToken? nextPageToken = null) : base(
code,
httpVersion,
responseHeaders,
responseTime,
responseLength,
@@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
/// <returns></returns>
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
{
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
}
/// <inheritdoc />
@@ -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");
}
}
@@ -0,0 +1,96 @@
using CryptoExchange.Net.Objects;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for requesting a transfer
/// </summary>
public class GetBalancesOptions : EndpointOptions<GetBalancesRequest>
{
/// <summary>
/// Supported account types
/// </summary>
public AccountTypeFilter[] SupportedAccountTypes { get; set; }
/// <summary>
/// ctor
/// </summary>
public GetBalancesOptions(params AccountTypeFilter[] accountTypes) : base(true)
{
SupportedAccountTypes = accountTypes;
}
/// <summary>
/// Validate a request
/// </summary>
public Error? ValidateRequest(
string exchange,
GetBalancesRequest request,
TradingMode[] supportedApiTypes)
{
if (request.AccountType != null && !IsValid(request.AccountType.Value))
return ArgumentError.Invalid(nameof(request.AccountType), "Invalid AccountType");
return base.ValidateRequest(exchange, request, null, supportedApiTypes);
}
/// <summary>
/// Is the account type valid for this client
/// </summary>
/// <param name="accountType"></param>
/// <returns></returns>
public bool IsValid(SharedAccountType accountType)
{
if (accountType == SharedAccountType.Funding)
return SupportedAccountTypes.Contains(AccountTypeFilter.Funding);
if (accountType == SharedAccountType.Spot)
return SupportedAccountTypes.Contains(AccountTypeFilter.Spot);
if (accountType == SharedAccountType.PerpetualLinearFutures
|| accountType == SharedAccountType.PerpetualInverseFutures
|| accountType == SharedAccountType.DeliveryLinearFutures
|| accountType == SharedAccountType.DeliveryInverseFutures)
{
return SupportedAccountTypes.Contains(AccountTypeFilter.Futures);
}
if (accountType == SharedAccountType.CrossMargin
|| accountType == SharedAccountType.IsolatedMargin)
{
return SupportedAccountTypes.Contains(AccountTypeFilter.Margin);
}
return SupportedAccountTypes.Contains(AccountTypeFilter.Option);
}
}
/// <summary>
/// Account type filter
/// </summary>
public enum AccountTypeFilter
{
/// <summary>
/// Funding account
/// </summary>
Funding,
/// <summary>
/// Spot account
/// </summary>
Spot,
/// <summary>
/// Futures account
/// </summary>
Futures,
/// <summary>
/// Margin account
/// </summary>
Margin,
/// <summary>
/// Option account
/// </summary>
Option
}
}
@@ -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);
}

Some files were not shown because too many files have changed in this diff Show More