using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
///
/// Options and description of a Shared API capability.
///
public abstract class CapabilityOptions
{
///
/// Request parameter rules for the capability. The rules differ per exchange API and can be used to
/// dynamically discover and adjust the request parameters for a capability.
///
public RequestParameterDescription[] RequestParameterRules { get; private set; } = [];
///
/// Exchange parameter rules for the capability. These are parameters specific to the exchange API and
/// must be provided in the request via the ExchangeParameters property of the request object.
///
public ExchangeParameterDescription[] ExchangeParameterRules { get; set; } = [];
private readonly RequestParameterDescription[] _defaultParameterRules;
///
/// Deprectaed, use RequestParameterRules instead.
///
public List RequiredOptionalParameters
{
get => RequestParameterRules.Where(x => x.DefaultSupport == RequestParameterSupport.Optional
&& x.Support == RequestParameterSupport.Required)
.Cast()
.ToList();
}
///
/// Deprectaed, use ExchangeParameterRules instead.
///
public List RequiredExchangeParameters
{
get => ExchangeParameterRules.Where(x => x.Requirement == ExchangeParameterRequirement.Required).Cast().ToList();
}
///
/// Deprectaed, use ExchangeParameterRules instead.
///
public List OptionalExchangeParameters
{
get => ExchangeParameterRules.Where(x => x.Requirement == ExchangeParameterRequirement.Optional).Cast().ToList();
}
///
/// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters)
///
public bool SupportsMultipleSymbols { get; set; } = false;
///
/// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters)
///
public int? MaxSymbolCount { get; set; }
///
/// Exchange
///
public string Exchange { get; set; }
///
/// Operation name
///
public string OperationName { get; set; }
///
/// Exchange specific additional info
///
public string? RequestNotes { get; set; }
///
/// Whether the call requires authentication
///
public bool NeedsAuthentication { get; set; }
///
/// Whether the call is supported. If false the exchange API does not support this operation.
///
public bool Supported { get; set; } = true;
///
/// Description of the endpoint
///
public abstract string Description { get; }
///
/// The type of the capability
///
public abstract Type CapabilityType { get; }
///
/// ctor
///
public CapabilityOptions(
string exchange,
string operationName,
bool needAuthentication,
IEnumerable defaultParameterRules)
{
Exchange = exchange;
OperationName = operationName;
NeedsAuthentication = needAuthentication;
_defaultParameterRules = defaultParameterRules
.Select(x => x.WithSupport(x.DefaultSupport))
.ToArray();
RequestParameterRules = CombineParameters(
_defaultParameterRules,
Array.Empty());
}
///
/// Set request parameter overrides for this instance
///
/// The parameter overrides
protected void SetRequestParameters(IEnumerable overrides)
{
RequestParameterRules = CombineParameters(_defaultParameterRules, overrides);
}
private RequestParameterDescription[] CombineParameters(
RequestParameterDescription[] defaultParameters,
IEnumerable overrideParameters
)
{
var result = defaultParameters
.Select(x => x.WithSupport(x.DefaultSupport))
.ToList();
foreach (var item in overrideParameters)
{
var index = result.FindIndex(
x => x.Name == item.ParameterName);
if (index == -1)
{
throw new ArgumentException(
$"No default request parameter rule exists for " +
$"`{item.ParameterName}`");
}
result[index] = result[index].WithOverride(item);
}
return result.ToArray();
}
///
/// Validate a request
///
/// Provided exchange parameters
/// Request trading mode
/// Supported trading modes
///
public virtual Error? ValidateRequest(ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
{
if (!Supported)
return ArgumentError.Invalid("Operation", $"Operation {Exchange} {OperationName} is not supported by the API");
if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value))
return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
foreach (var param in RequiredExchangeParameters)
{
if (ExchangeParameters.HasValue(exchangeParameters, Exchange, param.Name, param.ValueType) != true
&& param.Aliases.Any(x => ExchangeParameters.HasValue(exchangeParameters, Exchange, x, param.ValueType) != true))
{
if (param.Aliases.Length == 0)
return ArgumentError.Invalid(param.Name, $"Exchange parameter `{param.Name}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
else
return ArgumentError.Invalid(string.Join("/", [param.Name, .. param.Aliases]), $"One of exchange parameters `{string.Join(", ", [param.Name, .. param.Aliases])}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
}
}
return null;
}
///
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"{OperationName}");
if (!Supported)
{
sb.AppendLine(" NOT SUPPORTED");
return sb.ToString();
}
sb.AppendLine($"{Description}");
if (!string.IsNullOrEmpty(RequestNotes))
{
sb.Append(" Notes: ");
sb.AppendLine(RequestNotes);
}
if (RequestParameterRules.Any())
{
sb.AppendLine($" Request parameters:");
foreach (var param in RequestParameterRules)
sb.AppendLine($" {param}");
}
if (ExchangeParameterRules.Any())
{
sb.AppendLine($" Exchange specific parameters:");
foreach (var param in ExchangeParameterRules)
sb.AppendLine($" {param}");
}
sb.Append(" Needs authentication: ");
sb.AppendLine($"{NeedsAuthentication}");
return sb.ToString();
}
}
///
/// Options and description of a Shared API capability.
///
public abstract class CapabilityOptions<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
#endif
#pragma warning disable CS0618 // Type or member is obsolete
TRequest, TCapability> : EndpointOptions // Derives from endpoint options for compatibility, but can derive from CapabilityOptions in a later release
#pragma warning restore CS0618 // Type or member is obsolete
where TRequest : SharedRequest
where TCapability : ISharedApiCapability
{
private static PropertyInfo[] _requestProperties = typeof(TRequest).GetProperties();
private static readonly ConcurrentDictionary<(string Exchange, string Operation, string Parameter), byte> _loggedUnsupportedParameters = new();
///
public override Type CapabilityType => typeof(TCapability);
///
public virtual RequestParameterRuleOverride[] ParameterRuleOverwrites
{
set => SetRequestParameters(value);
}
///
/// ctor
///
public CapabilityOptions(string exchange, bool needsAuthentication, string requestName, IEnumerable defaultParameterRules)
: base(exchange, requestName, needsAuthentication, defaultParameterRules)
{
}
///
/// Validate a request
///
/// The request
/// Containing client
///
public virtual Error? ValidateRequest(TRequest request, TCapability client)
{
if (NeedsAuthentication && !client.Authenticated)
return new NoApiCredentialsError();
foreach (var param in RequestParameterRules)
{
var property = _requestProperties.Single(x => x.Name == param.Name);
var value = property.GetValue(request);
if (param.Support == RequestParameterSupport.Required)
{
if (value == null)
{
return ArgumentError.Invalid(
param.Name,
$"Request parameter `{param.Name}` for exchange `{Exchange}` is required and should be provided. Example: {param.ExampleValue}");
}
}
else if (param.Support == RequestParameterSupport.NotSupported)
{
if (value != null)
{
// For backwards compatibility don't return an error for provided unsupported parameters, only log a warning.
// In a future release this will be changed to return an error.
//return ArgumentError.Invalid(
// param.Name,
// $"Request parameter `{param.Name}` is not supported by exchange `{Exchange}`");
var warningKey = (Exchange, OperationName, param.Name);
if (_loggedUnsupportedParameters.TryAdd(warningKey, 0))
{
LibraryHelpers.StaticLogger?.LogWarning("The parameter {ParamName} has a supplied value for {Exchange}.{CapabilityName}, but is not supported. " +
"In a future major release this will return an error. To check supported/required parameters use the option RequestParameterRules.",
param.Name, Exchange, OperationName);
}
}
}
}
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");
}
}
return ValidateRequest(request.ExchangeParameters, request.TradingMode, client.SupportedTradingModes);
}
}
}