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); } } }