From e823114623926ffe9108d072e48f5037814e2938 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 29 Jun 2026 10:38:09 +0200 Subject: [PATCH] CryptoExchange V12 (#281) * Result types: * (Web)CallResult types are replaced by HttpResult, WebSocketResult and QueryResult with the same logic * Updated result types to record type * Result creation can be done with (Http/WebSocket/Query)Result.Ok(..) and .Fail(..) * Removed implicit result type conversion to bool, `if (result)` no longer works, instead use `if (result.Success)` * Replaced CallResult.SuccessResult with CallResult.Ok() * Fixed result object nullability hinting, for example Data might be null if Success isn't checked for true * Parameters & serialization: * Added support for `enabled` and `disabled` strings to bool converter * Removed ParameterCollection type, has been replaced by Parameters type * Removed ArraySerialization, OrderParameters and ParameterOrderComparer properties from RestApiClient, moved to ParameterSerializationsSettings * Updated RestRequestConfiguration in AuthenticationProvider.ProcessRequest to contain the full RequestDefinition instead of copied fields * Clients: * Updated Api client constructor logging parameter from ILogger to ILoggerFactory? * Added Api client constructor exchange name parameter * Added ToString overrides on base API types * Added Exchange property on BaseApiClient * Added ApiCredentials property on IRestApiClient and ISocketApiClient interfaces * Updated ILogger source from client name to topic specific client name * Removed logging from client creation * Fixed BaseRestClient SetApiCredentials not marked as virtual * Rest: * Added BaseAddress to RequestDefinition object * Updated RestApiClient AuthenticationProvider logic from private to protected and virtual * Removed RestApiClient.SendAsync baseAddress parameter removed * Removed RestApiClient.SendAsync without type parameter * WebSocket: * Updated MessageRouting definition into CreateForEvent for subscriptions and CreateForQuery for queries * Improved Query type safety with CeateForQuery which allows second parameter for specifying the result type * Renamed MessageRouter.CreateWithoutHandler to CreateVoid * Updated SocketApiClient.GetSocketConnection to check connection uri instead of Tag for finding compatible connections * Removed unused UnhandledMessageExpected property SocketApiClient * Fixed issue in SocketApiClient.GetSocketConnection causing requests to always wait the full max 10 seconds when there was a reconnecting socket * Shared APIs: * Updated Option definitions to always require the exchange name as first parameter * Added missing dedicated option types * Added Discover method on ISharedClient interface, returning info on supported capabilities and operations * Added SharedRequest GetParamValue helper method accepting multiple parameter names * Added ResetStaticExchangeParameters method on ExchangeParameters * Added Status property to SharedWithdrawal model * Added TradingModes property to SharedBalance model * Updated ExchangeSymbolCache to support multiple environments and additional key separation * Updated Shared ExchangeParameters parameter names to be case insensitive * Updated code comments * Replaced ExchangeResult with ExchangeCallResult type * Removed AsExchangeResult/ExchangeWebResult * Removed TradingMode from the response model, only maintained on models where it makes sense * Removed IListenKey support, listen keys now rely on internal management with TokenManager * Rate limiting: * Fixed websocket connection attempts counting towards rate limit even when server could not be reached * Removed host from rate limit methods, now part of the already provided RequestDefinition * Added amount parameter to RateLimit Reset method to allow partially resetting the limit * Added TokenManager implementation for automatic listenkey/token management * Added UserClientProvider base class * Added async streaming on UserDataTracker items with StreamUpdatesAsync * Added cancellation token support to UserDataTracker starting * Added Unit type for non-result types * Added ServerError constructor taking ErrorType and message to make it easier to create * Added SupportedEnvironments property to PlatformInfo * Updated SymbolOrderBook DoResyncAsync to return CallResult instead of CallResult which was redundant * Various small performance improvements --- .cursor/rules/cryptoexchange-net.mdc | 2 +- .github/copilot-instructions.md | 2 +- AGENTS.md | 2 +- .../CallResultTests.cs | 124 +--- .../ClientTests/RestClientTests.cs | 2 +- .../ClientTests/SocketClientTests.cs | 2 +- .../ExchangeSymbolCacheTests.cs | 220 +++++-- .../Implementations/TestQuery.cs | 8 +- .../Implementations/TestRestApiClient.cs | 10 +- .../Implementations/TestRestClient.cs | 4 +- .../Implementations/TestSerializerContext.cs | 4 +- .../Implementations/TestSocketApiClient.cs | 10 +- .../Implementations/TestSocketClient.cs | 4 +- .../Implementations/TestSubscription.cs | 4 +- .../ParameterCollectionTests.cs | 227 +++---- .../RateLimitTests.cs | 84 +-- .../SocketRoutingTests/QueryRouterTests.cs | 48 +- .../SocketRoutingTests/RoutingTableTests.cs | 16 +- .../SubscriptionRouterTests.cs | 34 +- .../SymbolOrderBookTests.cs | 2 +- .../TokenManagementTests.cs | 347 ++++++++++ .../Attributes/NullableAttributes.cs | 27 +- .../Authentication/AuthenticationProvider.cs | 6 +- CryptoExchange.Net/Clients/BaseApiClient.cs | 33 +- CryptoExchange.Net/Clients/BaseClient.cs | 8 +- CryptoExchange.Net/Clients/BaseRestClient.cs | 2 +- CryptoExchange.Net/Clients/RestApiClient.cs | 262 ++++---- CryptoExchange.Net/Clients/SocketApiClient.cs | 372 ++++++----- .../Clients/UserClientProvider.cs | 172 +++++ .../SystemTextJson/BoolConverter.cs | 28 +- .../SystemTextJson/DateTimeConverter.cs | 15 +- CryptoExchange.Net/CryptoExchange.Net.csproj | 6 +- CryptoExchange.Net/ExchangeHelpers.cs | 18 +- CryptoExchange.Net/ExchangeSymbolCache.cs | 252 ++++++-- CryptoExchange.Net/ExtensionMethods.cs | 11 +- .../Interfaces/Clients/IBaseApiClient.cs | 4 + .../Interfaces/Clients/IRestApiClient.cs | 5 + .../Interfaces/Clients/ISocketApiClient.cs | 6 + .../Interfaces/ISymbolOrderBook.cs | 2 +- .../RateLimitGateLoggingExtensions.cs | 12 +- .../RestApiClientLoggingExtensions.cs | 26 +- .../SocketApiClientLoggingExtension.cs | 20 +- .../SocketConnectionLoggingExtension.cs | 2 +- CryptoExchange.Net/Objects/CallResult.cs | 592 ------------------ CryptoExchange.Net/Objects/Error.cs | 8 + .../Objects/Options/ExchangeOptions.cs | 2 +- .../Objects/Options/RestExchangeOptions.cs | 8 +- .../Objects/Options/SocketExchangeOptions.cs | 8 +- .../Objects/ParameterCollection.cs | 343 ---------- .../Objects/ParameterSerializationSettings.cs | 146 +++++ CryptoExchange.Net/Objects/Parameters.cs | 375 +++++++++++ CryptoExchange.Net/Objects/PlatformInfo.cs | 15 +- .../Objects/RequestDefinition.cs | 29 +- .../Objects/RequestDefinitionCache.cs | 60 +- .../Objects/RestRequestConfiguration.cs | 41 +- .../Objects/Results/CallResult.cs | 117 ++++ .../Objects/Results/HttpResult.cs | 286 +++++++++ .../Objects/Results/ICallResult.cs | 43 ++ .../Objects/Results/IHttpResult.cs | 85 +++ .../Objects/Results/IWebSocketResult.cs | 72 +++ CryptoExchange.Net/Objects/Results/Unit.cs | 20 + .../Objects/Results/WebSocketResult.cs | 320 ++++++++++ .../OrderBook/SymbolOrderBook.cs | 52 +- .../Filters/AuthenticatedEndpointFilter.cs | 2 +- .../RateLimiting/Filters/ExactPathFilter.cs | 2 +- .../RateLimiting/Filters/ExactPathsFilter.cs | 2 +- .../RateLimiting/Filters/HostFilter.cs | 4 +- .../Filters/LimitItemTypeFilter.cs | 2 +- .../RateLimiting/Filters/PathStartFilter.cs | 2 +- .../RateLimiting/Guards/RateLimitGuard.cs | 36 +- .../RateLimiting/Guards/RetryAfterGuard.cs | 6 +- .../RateLimiting/Guards/SingleLimitGuard.cs | 22 +- .../RateLimiting/Interfaces/IGuardFilter.cs | 3 +- .../RateLimiting/Interfaces/IRateLimitGate.cs | 10 +- .../Interfaces/IRateLimitGuard.cs | 10 +- .../RateLimiting/Interfaces/IWindowTracker.cs | 2 +- .../RateLimiting/RateLimitEvent.cs | 7 +- .../RateLimiting/RateLimitGate.cs | 31 +- .../Trackers/DecayWindowTracker.cs | 13 +- .../Trackers/FixedAfterStartWindowTracker.cs | 24 +- .../Trackers/FixedWindowTracker.cs | 22 +- .../Trackers/SlidingWindowTracker.cs | 23 +- .../SharedApis/Interfaces/ISharedClient.cs | 10 +- .../Rest/Futures/IFundingRateRestClient.cs | 12 +- .../IFuturesOrderClientIdRestClient.cs | 21 +- .../Rest/Futures/IFuturesOrderRestClient.cs | 100 +-- .../Rest/Futures/IFuturesSymbolRestClient.cs | 19 +- .../Rest/Futures/IFuturesTickerRestClient.cs | 23 +- .../Rest/Futures/IFuturesTpSlRestClient.cs | 21 +- .../Futures/IFuturesTriggerOrderRestClient.cs | 32 +- .../Futures/IIndexPriceKlineRestClient.cs | 14 +- .../Rest/Futures/ILeverageRestClient.cs | 21 +- .../Rest/Futures/IMarkPriceKlineRestClient.cs | 14 +- .../Rest/Futures/IOpenInterestRestClient.cs | 13 +- .../Futures/IPositionHistoryRestClient.cs | 12 +- .../Rest/Futures/IPositionModeRestClient.cs | 19 +- .../Interfaces/Rest/IAssetsRestClient.cs | 23 +- .../Interfaces/Rest/IBalanceRestClient.cs | 11 +- .../Interfaces/Rest/IBookTickerRestClient.cs | 13 +- .../Interfaces/Rest/IDepositRestClient.cs | 21 +- .../Interfaces/Rest/IFeeRestClient.cs | 11 +- .../Interfaces/Rest/IKlineRestClient.cs | 12 +- .../Interfaces/Rest/IListenKeyRestClient.cs | 45 -- .../Interfaces/Rest/IOrderBookRestClient.cs | 11 +- .../Interfaces/Rest/IRecentTradeRestClient.cs | 11 +- .../Rest/ITradeHistoryRestClient.cs | 12 +- .../Interfaces/Rest/ITransferRestClient.cs | 11 +- .../Interfaces/Rest/IWithdrawRestClient.cs | 11 +- .../Interfaces/Rest/IWithdrawalRestClient .cs | 25 - .../Interfaces/Rest/IWithdrawalRestClient.cs | 29 + .../Rest/Spot/ISpotOrderClientIdRestClient.cs | 21 +- .../Rest/Spot/ISpotOrderRestClient.cs | 82 ++- .../Rest/Spot/ISpotSymbolRestClient.cs | 16 +- .../Rest/Spot/ISpotTickerRestClient.cs | 23 +- .../Rest/Spot/ISpotTriggerOrderRestClient.cs | 31 +- .../Futures/IFuturesOrderSocketClient.cs | 7 +- .../Socket/Futures/IPositionSocketClient.cs | 5 +- .../Interfaces/Socket/IBalanceSocketClient.cs | 7 +- .../Socket/IBookTickerSocketClient.cs | 7 +- .../Interfaces/Socket/IKlineSocketClient.cs | 3 +- .../Socket/IOrderBookSocketClient.cs | 5 +- .../Interfaces/Socket/ITickerSocketClient.cs | 5 +- .../Interfaces/Socket/ITickersSocketClient.cs | 5 +- .../Interfaces/Socket/ITradeSocketClient.cs | 7 +- .../Socket/IUserTradeSocketClient.cs | 7 +- .../Socket/Spot/ISpotOrderSocketClient.cs | 7 +- .../SharedApis/Models/ExchangeParameters.cs | 134 ++-- .../SharedApis/Models/ExchangeResult.cs | 55 -- .../SharedApis/Models/ExchangeWebResult.cs | 153 ----- ...ancelFuturesOrderByClientOrderIdOptions.cs | 15 + .../Endpoints/CancelFuturesOrderOptions.cs | 19 + .../Endpoints/CancelFuturesTpSlOptions.cs | 19 + .../CancelFuturesTriggerOrderOptions.cs | 19 + .../CancelSpotOrderByClientOrderIdOptions.cs | 19 + .../Endpoints/CancelSpotOrderOptions.cs | 19 + .../CancelSpotTriggerOrderOptions.cs | 19 + .../Options/Endpoints/ClosePositionOptions.cs | 19 + .../Options/Endpoints/EndpointOptions.cs | 139 ++-- .../Options/Endpoints/GetAssetOptions.cs | 19 + .../Options/Endpoints/GetAssetsOptions.cs | 19 + .../Options/Endpoints/GetBalancesOptions.cs | 11 +- .../Options/Endpoints/GetBookTickerOptions.cs | 19 + .../Endpoints/GetClosedOrdersOptions.cs | 54 -- .../Endpoints/GetDepositAddressesOptions.cs | 19 + .../Options/Endpoints/GetDepositsOptions.cs | 18 +- .../Models/Options/Endpoints/GetFeeOptions.cs | 19 + .../Endpoints/GetFundingRateHistoryOptions.cs | 18 +- .../GetFuturesClosedOrdersOptions.cs | 47 ++ .../GetFuturesOrderByClientOrderIdOptions.cs | 19 + .../Endpoints/GetFuturesOrderOptions.cs | 19 + .../Endpoints/GetFuturesOrderTradesOptions.cs | 19 + .../Endpoints/GetFuturesSymbolsOptions.cs | 19 + .../Endpoints/GetFuturesTickerOptions.cs | 31 + .../Endpoints/GetFuturesTickersOptions.cs | 31 + .../GetFuturesTriggerOrderOptions.cs | 19 + .../Endpoints/GetFuturesUserTradesOptions.cs | 46 ++ .../Endpoints/GetIndexPriceKlinesOptions.cs | 117 ++++ .../Options/Endpoints/GetKlinesOptions.cs | 23 +- .../Options/Endpoints/GetLeverageOptions.cs | 19 + .../Endpoints/GetMarkPriceKlinesOptions.cs | 117 ++++ .../Endpoints/GetOpenFuturesOrdersOptions.cs | 19 + .../Endpoints/GetOpenInterestOptions.cs | 19 + .../Endpoints/GetOpenSpotOrdersOptions.cs | 19 + .../Options/Endpoints/GetOrderBookOptions.cs | 20 +- .../Endpoints/GetPositionHistoryOptions.cs | 18 +- .../Endpoints/GetPositionModeOptions.cs | 4 +- .../Options/Endpoints/GetPositionsOptions.cs | 19 + .../Endpoints/GetRecentTradesOptions.cs | 13 +- .../Endpoints/GetSpotClosedOrdersOptions.cs | 46 ++ .../GetSpotOrderByClientOrderIdOptions.cs | 19 + .../Options/Endpoints/GetSpotOrderOptions.cs | 19 + .../Endpoints/GetSpotOrderTradesOptions.cs | 19 + .../Endpoints/GetSpotSymbolsOptions.cs | 19 + ...ckerOptions.cs => GetSpotTickerOptions.cs} | 10 +- ...ersOptions.cs => GetSpotTickersOptions.cs} | 10 +- .../Endpoints/GetSpotTriggerOrderOptions.cs | 19 + .../Endpoints/GetSpotUserTradesOptions.cs | 46 ++ .../Endpoints/GetTradeHistoryOptions.cs | 16 +- .../Options/Endpoints/GetUserTradesOptions.cs | 54 -- .../Endpoints/GetWithdrawalsOptions.cs | 22 +- .../Endpoints/PaginatedEndpointOptions.cs | 22 +- .../Endpoints/PlaceFuturesOrderOptions.cs | 22 +- .../PlaceFuturesTriggerOrderOptions.cs | 22 +- .../Endpoints/PlaceSpotOrderOptions.cs | 21 +- .../Endpoints/PlaceSpotTriggerOrderOptions.cs | 17 +- .../Endpoints/SetFuturesTpSlOptions.cs | 19 + .../Options/Endpoints/SetLeverageOptions.cs | 4 +- .../Endpoints/SetPositionModeOptions.cs | 4 +- .../Options/Endpoints/TransferOptions.cs | 12 +- .../Options/Endpoints/WithdrawOptions.cs | 4 +- .../Models/Options/ParameterDescription.cs | 15 +- .../Subscriptions/SubscribeBalanceOptions.cs | 19 + .../SubscribeBookTickerOptions.cs | 19 + .../SubscribeFuturesOrderOptions.cs | 19 + .../Subscriptions/SubscribeKlineOptions.cs | 11 +- .../SubscribeOrderBookOptions.cs | 8 +- .../Subscriptions/SubscribePositionOptions.cs | 19 + .../SubscribeSpotOrderOptions.cs | 19 + .../Subscriptions/SubscribeTickerOptions.cs | 4 +- .../Subscriptions/SubscribeTickersOptions.cs | 4 +- .../Subscriptions/SubscribeTradeOptions.cs | 19 + .../SubscribeUserTradeOptions.cs | 19 + .../SharedApis/Models/Rest/GetAssetRequest.cs | 3 +- .../Models/Rest/GetAssetsRequest.cs | 3 +- .../Models/Rest/GetBalancesRequest.cs | 6 +- .../Models/Rest/GetDepositAddressesRequest.cs | 3 +- .../Models/Rest/GetDepositsRequest.cs | 3 +- .../Models/Rest/GetOpenOrdersRequest.cs | 9 +- .../Models/Rest/GetPositionHistoryRequest.cs | 10 +- .../Models/Rest/GetPositionModeRequest.cs | 11 +- .../Models/Rest/GetPositionsRequest.cs | 9 +- .../Models/Rest/GetSymbolsRequest.cs | 8 +- .../Models/Rest/GetTickersRequest.cs | 8 +- .../Models/Rest/GetWithdrawalsRequest.cs | 3 +- .../Models/Rest/KeepAliveListenKeyRequest.cs | 29 - .../Models/Rest/PlaceFuturesOrderRequest.cs | 1 - .../Models/Rest/SetPositionModeRequest.cs | 10 +- .../Models/Rest/StartListenKeyRequest.cs | 23 - .../Models/Rest/StopListenKeyRequest.cs | 29 - .../SharedApis/Models/Rest/TransferRequest.cs | 2 +- .../SharedApis/Models/Rest/WithdrawRequest.cs | 3 +- .../SharedApis/Models/SharedRequest.cs | 30 +- .../SharedApis/Models/SharedSymbolRequest.cs | 28 +- .../Socket/SubscribeAllTickersRequest.cs | 8 +- .../Models/Socket/SubscribeBalancesRequest.cs | 15 +- .../Socket/SubscribeFuturesOrderRequest.cs | 15 +- .../Models/Socket/SubscribeKlineRequest.cs | 6 +- .../Socket/SubscribeOrderBookRequest.cs | 6 +- .../Models/Socket/SubscribePositionRequest.cs | 15 +- .../Socket/SubscribeSpotOrderRequest.cs | 10 +- .../Models/Socket/SubscribeTickerRequest.cs | 6 +- .../Models/Socket/SubscribeTradeRequest.cs | 6 +- .../Socket/SubscribeUserTradeRequest.cs | 15 +- .../ResponseModels/SharedBalance.cs | 13 +- .../ResponseModels/SharedWithdrawal.cs | 8 +- .../SharedApis/SharedClientInfo.cs | 62 ++ CryptoExchange.Net/SharedApis/SharedUtils.cs | 180 ++++++ .../Default/CryptoExchangeWebSocketClient.cs | 39 +- .../Sockets/Default/Routing/MessageRoute.cs | 169 ++++- .../Sockets/Default/Routing/MessageRouter.cs | 205 +++--- .../Default/Routing/SubscriptionRouter.cs | 2 +- .../Sockets/Default/SocketConnection.cs | 193 ++++-- .../Sockets/Default/Subscription.cs | 29 + .../HighPerf/HighPerfJsonSocketConnection.cs | 5 +- .../HighPerfJsonSocketConnectionFactory.cs | 4 +- .../HighPerf/HighPerfSocketConnection.cs | 29 +- .../HighPerf/HighPerfWebSocketClient.cs | 12 +- .../Interfaces/IHighPerfConnectionFactory.cs | 2 +- .../Sockets/Interfaces/ISocketConnection.cs | 2 +- .../Sockets/PeriodicTaskRegistration.cs | 2 +- CryptoExchange.Net/Sockets/Query.cs | 48 +- .../Comparers/SystemTextJsonComparer.cs | 4 +- .../Testing/Implementations/TestSocket.cs | 2 +- .../Testing/RestIntegrationTest.cs | 6 +- .../Testing/RestRequestValidator.cs | 16 +- .../Testing/SharedRestRequestValidator.cs | 10 +- .../Testing/SocketIntegrationTest.cs | 4 +- .../Testing/SocketRequestValidator.cs | 6 +- .../Testing/SocketSubscriptionValidator.cs | 18 +- CryptoExchange.Net/Testing/TestHelpers.cs | 37 +- .../TokenManagement/TokenInfo.cs | 104 +++ .../TokenManagement/TokenLease.cs | 45 ++ .../TokenManagement/TokenManagementType.cs | 17 + .../TokenManagement/TokenManager.cs | 71 +++ .../TokenManagement/TokenOperations.cs | 26 + .../TokenManagement/TokenRegistry.cs | 324 ++++++++++ .../TokenManagement/TokenRegistryProvider.cs | 27 + .../TokenManagement/TokenScope.cs | 68 ++ .../Trackers/Klines/KlineTracker.cs | 20 +- .../Trackers/Trades/TradeTracker.cs | 22 +- .../UserData/Interfaces/IUserDataTracker.cs | 8 + .../Interfaces/IUserFuturesDataTracker.cs | 3 +- .../Interfaces/IUserSpotDataTracker.cs | 8 +- .../UserData/ItemTrackers/BalanceTracker.cs | 6 +- .../ItemTrackers/FuturesOrderTracker.cs | 8 +- .../ItemTrackers/FuturesUserTradeTracker.cs | 6 +- .../UserData/ItemTrackers/PositionTracker.cs | 6 +- .../UserData/ItemTrackers/SpotOrderTracker.cs | 8 +- .../ItemTrackers/SpotUserTradeTracker.cs | 6 +- .../ItemTrackers/UserDataItemTracker.cs | 62 +- .../Trackers/UserData/UserDataTracker.cs | 23 +- .../UserData/UserFuturesDataTracker.cs | 65 +- .../Trackers/UserData/UserSpotDataTracker.cs | 66 +- README.md | 38 ++ llms.txt | 2 +- 285 files changed, 7029 insertions(+), 3658 deletions(-) create mode 100644 CryptoExchange.Net.UnitTests/TokenManagementTests.cs create mode 100644 CryptoExchange.Net/Clients/UserClientProvider.cs delete mode 100644 CryptoExchange.Net/Objects/CallResult.cs delete mode 100644 CryptoExchange.Net/Objects/ParameterCollection.cs create mode 100644 CryptoExchange.Net/Objects/ParameterSerializationSettings.cs create mode 100644 CryptoExchange.Net/Objects/Parameters.cs create mode 100644 CryptoExchange.Net/Objects/Results/CallResult.cs create mode 100644 CryptoExchange.Net/Objects/Results/HttpResult.cs create mode 100644 CryptoExchange.Net/Objects/Results/ICallResult.cs create mode 100644 CryptoExchange.Net/Objects/Results/IHttpResult.cs create mode 100644 CryptoExchange.Net/Objects/Results/IWebSocketResult.cs create mode 100644 CryptoExchange.Net/Objects/Results/Unit.cs create mode 100644 CryptoExchange.Net/Objects/Results/WebSocketResult.cs delete mode 100644 CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs delete mode 100644 CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs create mode 100644 CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderByClientOrderIdOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTpSlOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTriggerOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderByClientOrderIdOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotTriggerOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/ClosePositionOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetsOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBookTickerOptions.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositAddressesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFeeOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesClosedOrdersOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderByClientOrderIdOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderTradesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesSymbolsOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickerOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickersOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTriggerOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesUserTradesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetIndexPriceKlinesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetLeverageOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetMarkPriceKlinesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenFuturesOrdersOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenInterestOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenSpotOrdersOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionsOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotClosedOrdersOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderByClientOrderIdOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderTradesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotSymbolsOptions.cs rename CryptoExchange.Net/SharedApis/Models/Options/Endpoints/{GetTickerOptions.cs => GetSpotTickerOptions.cs} (55%) rename CryptoExchange.Net/SharedApis/Models/Options/Endpoints/{GetTickersOptions.cs => GetSpotTickersOptions.cs} (55%) create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTriggerOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotUserTradesOptions.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetFuturesTpSlOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBalanceOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBookTickerOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeFuturesOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribePositionOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeSpotOrderOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTradeOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeUserTradeOptions.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs delete mode 100644 CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs create mode 100644 CryptoExchange.Net/SharedApis/SharedClientInfo.cs create mode 100644 CryptoExchange.Net/SharedApis/SharedUtils.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenInfo.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenLease.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenManagementType.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenManager.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenOperations.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenRegistry.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenRegistryProvider.cs create mode 100644 CryptoExchange.Net/TokenManagement/TokenScope.cs diff --git a/.cursor/rules/cryptoexchange-net.mdc b/.cursor/rules/cryptoexchange-net.mdc index 3f22d6df..f1952fc5 100644 --- a/.cursor/rules/cryptoexchange-net.mdc +++ b/.cursor/rules/cryptoexchange-net.mdc @@ -32,7 +32,7 @@ var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol)); ## Result pattern -Same `WebCallResult` / `CallResult` everywhere. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging. +REST methods return `HttpResult` and websocket subscription methods return `WebSocketResult`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging. ## Available shared interfaces diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a377d9ee..899b447b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -30,7 +30,7 @@ For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `A ## Result pattern -Every method returns `WebCallResult` or `CallResult`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded. +REST methods return `HttpResult` and websocket subscription methods return `WebSocketResult`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded. ## Available shared interfaces diff --git a/AGENTS.md b/AGENTS.md index 027ddf96..c3e47557 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ Each exchange documents which interfaces it implements (some exchanges don't sup ## Core Pattern: Result Handling -Same as exchange-specific libraries — `WebCallResult` (REST) or `CallResult` (WebSocket) with `.Success`, `.Data`, `.Error`. Always check `.Success` first. +Same as exchange-specific libraries: REST calls return `HttpResult` and websocket subscription calls return `WebSocketResult`, both with `.Success`, `.Data`, and `.Error`. Always check `.Success` first. ```csharp var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol)); diff --git a/CryptoExchange.Net.UnitTests/CallResultTests.cs b/CryptoExchange.Net.UnitTests/CallResultTests.cs index 27860bb9..c218dc62 100644 --- a/CryptoExchange.Net.UnitTests/CallResultTests.cs +++ b/CryptoExchange.Net.UnitTests/CallResultTests.cs @@ -14,157 +14,41 @@ namespace CryptoExchange.Net.UnitTests [Test] public void TestBasicErrorCallResult() { - var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); + var result = CallResult.Fail(new ServerError("TestError", ErrorInfo.Unknown)); ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError"); - ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result.Success); } [Test] public void TestBasicSuccessCallResult() { - var result = new CallResult(null); + var result = CallResult.Ok(); ClassicAssert.IsNull(result.Error); - Assert.That(result); Assert.That(result.Success); } [Test] public void TestCallResultError() { - var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); + var result = CallResult.Fail(new ServerError("TestError", ErrorInfo.Unknown)); ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError"); ClassicAssert.IsNull(result.Data); - ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result.Success); } [Test] public void TestCallResultSuccess() { - var result = new CallResult(new object()); + var result = CallResult.Ok(new object()); ClassicAssert.IsNull(result.Error); ClassicAssert.IsNotNull(result.Data); - Assert.That(result); Assert.That(result.Success); } - [Test] - public void TestCallResultSuccessAs() - { - var result = new CallResult(new TestObjectResult()); - var asResult = result.As(result.Data.InnerData); - - ClassicAssert.IsNull(asResult.Error); - ClassicAssert.IsNotNull(asResult.Data); - Assert.That(asResult.Data is not null); - Assert.That(asResult); - Assert.That(asResult.Success); - } - - [Test] - public void TestCallResultErrorAs() - { - var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); - var asResult = result.As(default); - - ClassicAssert.IsNotNull(asResult.Error); - ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError"); - ClassicAssert.IsNull(asResult.Data); - ClassicAssert.IsFalse(asResult); - ClassicAssert.IsFalse(asResult.Success); - } - - [Test] - public void TestCallResultErrorAsError() - { - var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); - var asResult = result.AsError(new ServerError("TestError2", ErrorInfo.Unknown)); - - ClassicAssert.IsNotNull(asResult.Error); - ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2"); - ClassicAssert.IsNull(asResult.Data); - ClassicAssert.IsFalse(asResult); - ClassicAssert.IsFalse(asResult.Success); - } - - [Test] - public void TestWebCallResultErrorAsError() - { - var result = new WebCallResult(new ServerError("TestError", ErrorInfo.Unknown)); - var asResult = result.AsError(new ServerError("TestError2", ErrorInfo.Unknown)); - - ClassicAssert.IsNotNull(asResult.Error); - ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2"); - ClassicAssert.IsNull(asResult.Data); - ClassicAssert.IsFalse(asResult); - ClassicAssert.IsFalse(asResult.Success); - } - - [Test] - public void TestWebCallResultSuccessAsError() - { - var result = new WebCallResult( - System.Net.HttpStatusCode.OK, - HttpVersion.Version11, - new HttpResponseMessage().Headers, - TimeSpan.FromSeconds(1), - null, - "{}", - 1, - "https://test.com/api", - null, - HttpMethod.Get, - new HttpRequestMessage().Headers, - ResultDataSource.Server, - new TestObjectResult(), - null); - var asResult = result.AsError(new ServerError("TestError2", ErrorInfo.Unknown)); - - ClassicAssert.IsNotNull(asResult.Error); - 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"); - Assert.That(asResult.RequestMethod == HttpMethod.Get); - ClassicAssert.IsNull(asResult.Data); - ClassicAssert.IsFalse(asResult); - ClassicAssert.IsFalse(asResult.Success); - } - - [Test] - public void TestWebCallResultSuccessAsSuccess() - { - var result = new WebCallResult( - System.Net.HttpStatusCode.OK, - HttpVersion.Version11, - new HttpResponseMessage().Headers, - TimeSpan.FromSeconds(1), - null, - "{}", - 1, - "https://test.com/api", - null, - HttpMethod.Get, - new HttpRequestMessage().Headers, - ResultDataSource.Server, - new TestObjectResult(), - null); - var asResult = result.As(result.Data.InnerData); - - ClassicAssert.IsNull(asResult.Error); - Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK); - Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); - Assert.That(asResult.RequestUrl == "https://test.com/api"); - Assert.That(asResult.RequestMethod == HttpMethod.Get); - ClassicAssert.IsNotNull(asResult.Data); - Assert.That(asResult); - Assert.That(asResult.Success); - } } public class TestObjectResult diff --git a/CryptoExchange.Net.UnitTests/ClientTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/ClientTests/RestClientTests.cs index c766d7d0..54ade79f 100644 --- a/CryptoExchange.Net.UnitTests/ClientTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/ClientTests/RestClientTests.cs @@ -134,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests client.ApiClient1.SetParameterPosition(httpMethod, pos); client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK); - var result = await client.ApiClient1.GetResponseAsync(httpMethod, new ParameterCollection + var result = await client.ApiClient1.GetResponseAsync(httpMethod, new Parameters(new ParameterSerializationSettings()) { { "TestParam1", "Value1" }, { "TestParam2", 2 }, diff --git a/CryptoExchange.Net.UnitTests/ClientTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/ClientTests/SocketClientTests.cs index 51268093..5424e9bf 100644 --- a/CryptoExchange.Net.UnitTests/ClientTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/ClientTests/SocketClientTests.cs @@ -110,7 +110,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests var result = await client.ApiClient1.SubscribeToUpdatesAsync(x => {}, false, default); // act - await client.UnsubscribeAsync(result.Data); + await client.UnsubscribeAsync(result.Data!); // assert Assert.That(socket.Connected == false); diff --git a/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs b/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs index 61dda9e4..4dbbe13a 100644 --- a/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs +++ b/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs @@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests var symbols = CreateTestSymbols(); // act - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); - var hasCached = ExchangeSymbolCache.HasCached(topicId); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); + var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null); // assert Assert.That(hasCached, Is.True); @@ -52,14 +52,14 @@ namespace CryptoExchange.Net.UnitTests var symbols = CreateTestSymbols(); // act - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // assert - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCEUR"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHBTC"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "XRPUSDT"), Is.True); } [Test] @@ -78,13 +78,13 @@ namespace CryptoExchange.Net.UnitTests }; // act - ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, initialSymbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, updatedSymbols); // assert - should still have only the initial symbol since less than 60 minutes passed - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True); // The second update should not have been applied - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.False); } [Test] @@ -95,8 +95,8 @@ namespace CryptoExchange.Net.UnitTests var symbols = Array.Empty(); // act - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); - var hasCached = ExchangeSymbolCache.HasCached(topicId); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); + var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null); // assert Assert.That(hasCached, Is.False); @@ -109,7 +109,7 @@ namespace CryptoExchange.Net.UnitTests var nonExistentTopic = "NonExistent_" + Guid.NewGuid(); // act - var result = ExchangeSymbolCache.HasCached(nonExistentTopic); + var result = ExchangeSymbolCache.HasCached(nonExistentTopic, "Env", null); // assert Assert.That(result, Is.False); @@ -121,10 +121,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeWithData"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.HasCached(topicId); + var result = ExchangeSymbolCache.HasCached(topicId, "Env", null); // assert Assert.That(result, Is.True); @@ -135,10 +135,10 @@ namespace CryptoExchange.Net.UnitTests { // arrange var topicId = "ExchangeNoData"; - ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty()); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, Array.Empty()); // act - var result = ExchangeSymbolCache.HasCached(topicId); + var result = ExchangeSymbolCache.HasCached(topicId, "Env", null); // assert Assert.That(result, Is.False); @@ -150,10 +150,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeSupports"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"); + var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"); // assert Assert.That(result, Is.True); @@ -165,10 +165,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeNoSupport"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT"); + var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "LINKUSDT"); // assert Assert.That(result, Is.False); @@ -181,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests var nonExistentTopic = "NonExistent_" + Guid.NewGuid(); // act - var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT"); + var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, "BTCUSDT"); // assert Assert.That(result, Is.False); @@ -193,11 +193,11 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeSharedSymbol"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); // act - var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol); + var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol); // assert Assert.That(result, Is.True); @@ -209,11 +209,11 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeNoSharedSymbol"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT"); // act - var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol); + var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol); // assert Assert.That(result, Is.False); @@ -225,11 +225,11 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeDifferentMode"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT"); // act - var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol); + var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol); // assert Assert.That(result, Is.False); @@ -243,7 +243,7 @@ namespace CryptoExchange.Net.UnitTests var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); // act - var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol); + var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, sharedSymbol); // assert Assert.That(result, Is.False); @@ -255,10 +255,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeBaseAsset"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC"); + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC"); // assert Assert.That(result, Is.Not.Null); @@ -273,10 +273,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeCaseInsensitive"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc"); + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "btc"); // assert Assert.That(result, Is.Not.Null); @@ -289,10 +289,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeNoBaseAsset"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK"); + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "LINK"); // assert Assert.That(result, Is.Not.Null); @@ -306,7 +306,7 @@ namespace CryptoExchange.Net.UnitTests var nonExistentTopic = "NonExistent_" + Guid.NewGuid(); // act - var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC"); + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "Env", null, "BTC"); // assert Assert.That(result, Is.Not.Null); @@ -319,10 +319,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeParse"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT"); + var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "BTCUSDT"); // assert Assert.That(result, Is.Not.Null); @@ -338,10 +338,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeNoParse"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT"); + var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "LINKUSDT"); // assert Assert.That(result, Is.Null); @@ -353,10 +353,10 @@ namespace CryptoExchange.Net.UnitTests // arrange var topicId = "ExchangeNullSymbol"; var symbols = CreateTestSymbols(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.ParseSymbol(topicId, null); + var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, null); // assert Assert.That(result, Is.Null); @@ -369,7 +369,7 @@ namespace CryptoExchange.Net.UnitTests var nonExistentTopic = "NonExistent_" + Guid.NewGuid(); // act - var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT"); + var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "Env", null, "BTCUSDT"); // assert Assert.That(result, Is.Null); @@ -391,14 +391,14 @@ namespace CryptoExchange.Net.UnitTests }; // act - ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1); - ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2); + ExchangeSymbolCache.UpdateSymbolInfo(topic1, "Env", null, symbols1); + ExchangeSymbolCache.UpdateSymbolInfo(topic2, "Env", null, symbols2); // assert - Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "BTCUSDT"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "ETHUSDT"), Is.False); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "ETHUSDT"), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "BTCUSDT"), Is.False); } [Test] @@ -411,14 +411,14 @@ namespace CryptoExchange.Net.UnitTests var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray(); // act - ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols); // assert var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT"); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True); - Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, spotSymbol), Is.True); + Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, futuresSymbol), Is.True); } [Test] @@ -429,10 +429,10 @@ namespace CryptoExchange.Net.UnitTests var spotSymbols = CreateTestSymbols(); var futuresSymbols = CreateFuturesSymbols(); var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray(); - ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols); // act - var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC"); + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC"); // assert Assert.That(result.Length, Is.GreaterThanOrEqualTo(2)); @@ -451,15 +451,119 @@ namespace CryptoExchange.Net.UnitTests new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot), new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot) }; - ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols); // act - var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH"); + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "ETH"); // assert Assert.That(result.Length, Is.EqualTo(3)); Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True); } + [Test] + public void GetSymbolsForBaseAsset_WithDifferentEnvironments_Should_ReturnNone() + { + // arrange + var topicId = "Topic1"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols); + + // act + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Test", null, "BTC"); + + // assert + Assert.That(result.Length, Is.EqualTo(0)); + } + + [Test] + public void GetSymbolsForBaseAsset_WithDifferentKey_Should_ReturnNone() + { + // arrange + var topicId = "Topic2"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols); + + // act + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC"); + + // assert + Assert.That(result.Length, Is.EqualTo(0)); + } + + [Test] + public void GetSymbolsForBaseAsset_WithSetKey_Should_ReturnNone() + { + // arrange + var topicId = "Topic3"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols); + + // act + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC"); + + // assert + Assert.That(result.Length, Is.EqualTo(0)); + } + + [Test] + public void GetSymbolsForBaseAsset_WithNotSetKey_Should_ReturnNone() + { + // arrange + var topicId = "Topic4"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "2", spotSymbols); + + // act + var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", null, "BTC"); + + // assert + Assert.That(result.Length, Is.EqualTo(2)); + } + + [Test] + public void ParseSymbol_WithDifferentKey_Should_ReturnNull() + { + // arrange + var topicId = "Topic5"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols); + + // act + var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT"); + + // assert + Assert.That(result, Is.Null); + } + + [Test] + public void ParseSymbol_WithSetKey_Should_ReturnNull() + { + // arrange + var topicId = "Topic6"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols); + + // act + var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT"); + + // assert + Assert.That(result, Is.Null); + } + + [Test] + public void ParseSymbol_WithNotSetKey_Should_ReturnNull() + { + // arrange + var topicId = "Topic7"; + var spotSymbols = CreateTestSymbols(); + ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols); + + // act + var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", null, "BTCUSDT"); + + // assert + Assert.That(result, Is.Not.Null); + } } } \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestQuery.cs b/CryptoExchange.Net.UnitTests/Implementations/TestQuery.cs index 05dd240d..9b223650 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestQuery.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestQuery.cs @@ -11,15 +11,15 @@ namespace CryptoExchange.Net.UnitTests.Implementations { public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1) { - MessageRouter = MessageRouter.CreateWithoutTopicFilter(request.Id.ToString(), HandleMessage); + MessageRouter = MessageRouter.CreateForQuery(request.Id.ToString(), HandleMessage); } - private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message) + private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message) { if (message.Data != "OK") - return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data })); + return CallResult.Fail(new ServerError(ErrorInfo.Unknown with { Message = message.Data })); - return CallResult.SuccessResult; + return CallResult.Ok(message); } } } diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs b/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs index 3b84c7e3..60ba85d4 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs @@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations { protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler(); - public TestRestApiClient(ILogger logger, HttpClient? httpClient, TestRestOptions options) - : base(logger, httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions) + public TestRestApiClient(ILoggerFactory? loggerFactory, HttpClient? httpClient, TestRestOptions options) + : base(loggerFactory, "Test", httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions) { } @@ -48,14 +48,14 @@ namespace CryptoExchange.Net.UnitTests.Implementations RequestFactory = factory; } - internal async Task> GetResponseAsync(HttpMethod? httpMethod = null, ParameterCollection? collection = null, RateLimitGate? rateLimitGate = null) + internal async Task> GetResponseAsync(HttpMethod? httpMethod = null, Parameters? collection = null, RateLimitGate? rateLimitGate = null) { - var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get) + var definition = new RequestDefinition(BaseAddress, "/path", httpMethod ?? HttpMethod.Get) { Weight = rateLimitGate == null ? 0 : 1, RateLimitGate = rateLimitGate }; - return await SendAsync(BaseAddress, definition, collection ?? new ParameterCollection(), default); + return await SendAsync(definition, collection ?? new Parameters(new ParameterSerializationSettings()), default); } internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos) diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/Implementations/TestRestClient.cs index e8122207..e6165e8f 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestRestClient.cs @@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations { Initialize(options.Value); - ApiClient1 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value)); - ApiClient2 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value)); + ApiClient1 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value)); + ApiClient2 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value)); } } } diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestSerializerContext.cs b/CryptoExchange.Net.UnitTests/Implementations/TestSerializerContext.cs index 6dd617cb..8ed694c2 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestSerializerContext.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestSerializerContext.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.UnitTests.ConverterTests; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.UnitTests.ConverterTests; using CryptoExchange.Net.UnitTests.Implementations; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -11,6 +12,7 @@ namespace CryptoExchange.Net.UnitTests [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(Parameters))] [JsonSerializable(typeof(TestObject))] [JsonSerializable(typeof(TestSocketMessage))] diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestSocketApiClient.cs b/CryptoExchange.Net.UnitTests/Implementations/TestSocketApiClient.cs index 6f4a0340..4dece6ab 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestSocketApiClient.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestSocketApiClient.cs @@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations { internal class TestSocketApiClient : SocketApiClient { - public TestSocketApiClient(ILogger logger, TestSocketOptions options) - : base(logger, options.Environment.SocketClientAddress, options, options.ExchangeOptions) + public TestSocketApiClient(ILoggerFactory? loggerFactory, TestSocketOptions options) + : base(loggerFactory, "Test", options.Environment.SocketClientAddress, options, options.ExchangeOptions) { } - public TestSocketApiClient(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions) - : base(logger, baseAddress, options, apiOptions) + public TestSocketApiClient(ILoggerFactory? loggerFactory, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions) + : base(loggerFactory, "Test", baseAddress, options, apiOptions) { } @@ -36,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) => new TestAuthenticationProvider(credentials); - public async Task> SubscribeToUpdatesAsync(Action> handler, bool subQuery, CancellationToken ct) + public async Task> SubscribeToUpdatesAsync(Action> handler, bool subQuery, CancellationToken ct) { return await base.SubscribeAsync(new TestSubscription(_logger, handler, subQuery, false), ct); } diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/Implementations/TestSocketClient.cs index 3b457293..6cb37eaf 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestSocketClient.cs @@ -19,8 +19,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations { Initialize(options.Value); - ApiClient1 = AddApiClient(new TestSocketApiClient(_logger, options.Value)); - ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, options.Value)); + ApiClient1 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value)); + ApiClient2 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value)); } } } diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestSubscription.cs b/CryptoExchange.Net.UnitTests/Implementations/TestSubscription.cs index 1ff2a4d0..ee6cbab5 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestSubscription.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestSubscription.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations _handler = handler; _subQuery = subQuery; - MessageRouter = MessageRouter.CreateWithoutTopicFilter("test", HandleUpdate); + MessageRouter = MessageRouter.CreateForEvent("test", HandleUpdate); } protected override Query? GetSubQuery(SocketConnection connection) @@ -44,7 +44,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data) { _handler(new DataEvent("Test", data, time, originalData)); - return CallResult.SuccessResult; + return CallResult.Ok(); } } } diff --git a/CryptoExchange.Net.UnitTests/ParameterCollectionTests.cs b/CryptoExchange.Net.UnitTests/ParameterCollectionTests.cs index 2a3f83b3..d095a64c 100644 --- a/CryptoExchange.Net.UnitTests/ParameterCollectionTests.cs +++ b/CryptoExchange.Net.UnitTests/ParameterCollectionTests.cs @@ -12,319 +12,248 @@ namespace CryptoExchange.Net.UnitTests [Test] public void AddingBasicValue_SetValueCorrectly() { - var parameters = new ParameterCollection(); + var parameters = new Parameters(new ParameterSerializationSettings()); parameters.Add("test", "value"); Assert.That(parameters["test"], Is.EqualTo("value")); } - [Test] - public void AddingBasicNullValue_ThrowsException() - { - var parameters = new ParameterCollection(); - Assert.Throws(() => parameters.Add("test", null!)); - } - - [Test] - public void AddingOptionalBasicValue_SetValueCorrectly() - { - var parameters = new ParameterCollection(); - parameters.AddOptional("test", "value"); - Assert.That(parameters["test"], Is.EqualTo("value")); - } - [Test] public void AddingOptionalBasicNullValue_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptional("test", null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", null); Assert.That(parameters.ContainsKey("test"), Is.False); } [Test] public void AddingDecimalValueAsString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddString("test", 0.1m); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", 0.1m, DecimalSerialization.String); Assert.That(parameters["test"], Is.EqualTo("0.1")); } [Test] - public void AddingOptionalDecimalValueAsString_SetValueCorrectly() + public void AddingDecimalValueAsString2_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalString("test", 0.1m); + var parameters = new Parameters(new ParameterSerializationSettings() + { + Decimal = DecimalSerialization.String + }); + parameters.Add("test", 0.1m); Assert.That(parameters["test"], Is.EqualTo("0.1")); } - [Test] - public void AddingOptionalDecimalNullValueAsString_DoesntSetValue() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalString("test", (decimal?)null); - Assert.That(parameters.ContainsKey("test"), Is.False); - } - - [Test] - public void AddingIntValueAsString_SetValueCorrectly() - { - var parameters = new ParameterCollection(); - parameters.AddString("test", 1); - Assert.That(parameters["test"], Is.EqualTo("1")); - } - - [Test] - public void AddingOptionalIntValueAsString_SetValueCorrectly() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalString("test", 1); - Assert.That(parameters["test"], Is.EqualTo("1")); - } - [Test] public void AddingOptionalIntNullValueAsString_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptionalString("test", (int?)null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", (int?)null); Assert.That(parameters.ContainsKey("test"), Is.False); } [Test] public void AddingLongValueAsString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddString("test", 1L); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", 1L, IntegerSerialization.String); Assert.That(parameters["test"], Is.EqualTo("1")); } [Test] public void AddingOptionalLongValueAsString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalString("test", 1L); + var parameters = new Parameters(new ParameterSerializationSettings() + { + Integer = IntegerSerialization.String + }); + parameters.Add("test", 1L); Assert.That(parameters["test"], Is.EqualTo("1")); } [Test] public void AddingOptionalLongNullValueAsString_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptionalString("test", (long?)null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", (long?)null); Assert.That(parameters.ContainsKey("test"), Is.False); } [Test] public void AddingMillisecondTimestamp_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsNumber); Assert.That(parameters["test"], Is.EqualTo(1735689600000)); } [Test] public void AddingOptionalMillisecondTimestamp_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings() + { + DateTimes = DateTimeSerialization.MillisecondsNumber + }); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); Assert.That(parameters["test"], Is.EqualTo(1735689600000)); } [Test] public void AddingOptionalMillisecondNullValue_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptionalMilliseconds("test", null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", (DateTime?)null); Assert.That(parameters.ContainsKey("test"), Is.False); } [Test] public void AddingMillisecondTimestampString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsString); Assert.That(parameters["test"], Is.EqualTo("1735689600000")); } [Test] public void AddingOptionalMillisecondTimestampString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings() + { + DateTimes = DateTimeSerialization.MillisecondsString + }); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); Assert.That(parameters["test"], Is.EqualTo("1735689600000")); } - [Test] - public void AddingOptionalMillisecondStringNullValue_DoesntSetValue() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalMillisecondsString("test", null); - Assert.That(parameters.ContainsKey("test"), Is.False); - } - [Test] public void AddingSecondTimestamp_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsNumber); Assert.That(parameters["test"], Is.EqualTo(1735689600)); } [Test] public void AddingOptionalSecondTimestamp_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings() + { + DateTimes = DateTimeSerialization.SecondsNumber + }); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); Assert.That(parameters["test"], Is.EqualTo(1735689600)); } - [Test] - public void AddingSecondNullValue_DoesntSetValue() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalSeconds("test", null); - Assert.That(parameters.ContainsKey("test"), Is.False); - } - [Test] public void AddingSecondTimestampString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsString); Assert.That(parameters["test"], Is.EqualTo("1735689600")); } [Test] public void AddingOptionalSecondTimestampString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var parameters = new Parameters(new ParameterSerializationSettings() + { + DateTimes = DateTimeSerialization.SecondsString + }); + parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); Assert.That(parameters["test"], Is.EqualTo("1735689600")); } - [Test] - public void AddingSecondStringNullValue_DoesntSetValue() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalSecondsString("test", null); - Assert.That(parameters.ContainsKey("test"), Is.False); - } - [Test] public void AddingEnum_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddEnum("test", TestEnum.Two); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", TestEnum.Two); Assert.That(parameters["test"], Is.EqualTo("2")); } [Test] public void AddingOptionalEnum_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalEnum("test", (TestEnum?)TestEnum.Two); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", (TestEnum?)TestEnum.Two); Assert.That(parameters["test"], Is.EqualTo("2")); } [Test] public void AddingOptionalEnumNullValue_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptionalEnum("test", (TestEnum?)null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", (TestEnum?)null); Assert.That(parameters.ContainsKey("test"), Is.False); } [Test] public void AddingEnumAsInt_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddEnumAsInt("test", TestEnum.Two); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", TestEnum.Two, EnumSerialization.Number); Assert.That(parameters["test"], Is.EqualTo(2)); } [Test] public void AddingOptionalEnumAsInt_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalEnumAsInt("test", (TestEnum?)TestEnum.Two); + var parameters = new Parameters(new ParameterSerializationSettings() + { + Enum = EnumSerialization.Number + }); + parameters.Add("test", TestEnum.Two); Assert.That(parameters["test"], Is.EqualTo(2)); } - [Test] - public void AddingOptionalEnumAsIntNullValue_DoesntSetValue() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalEnumAsInt("test", (TestEnum?)null); - Assert.That(parameters.ContainsKey("test"), Is.False); - } - [Test] public void AddingCommaSeparated_SetValueCorrectly() { - var parameters = new ParameterCollection(); + var parameters = new Parameters(new ParameterSerializationSettings()); parameters.AddCommaSeparated("test", ["1", "2"]); Assert.That(parameters["test"], Is.EqualTo("1,2")); } - [Test] - public void AddingOptionalCommaSeparated_SetValueCorrectly() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalCommaSeparated("test", ["1", "2"]); - Assert.That(parameters["test"], Is.EqualTo("1,2")); - } - [Test] public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptionalCommaSeparated("test", (string[]?)null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.AddCommaSeparated("test", (string[]?)null); Assert.That(parameters.ContainsKey("test"), Is.False); } [Test] public void AddingCommaSeparatedEnum_SetValueCorrectly() { - var parameters = new ParameterCollection(); + var parameters = new Parameters(new ParameterSerializationSettings()); parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]); Assert.That(parameters["test"], Is.EqualTo("2,1")); } - [Test] - public void AddingOptionalCommaSeparatedEnum_SetValueCorrectly() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalCommaSeparated("test", [TestEnum.Two, TestEnum.One]); - Assert.That(parameters["test"], Is.EqualTo("2,1")); - } - - [Test] - public void AddingOptionalCommaSeparatedEnumNullValue_DoesntSetValue() - { - var parameters = new ParameterCollection(); - parameters.AddOptionalCommaSeparated("test", (TestEnum[]?)null); - Assert.That(parameters.ContainsKey("test"), Is.False); - } - [Test] public void AddingBoolString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddBoolString("test", true); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", true, BoolSerialization.String); Assert.That(parameters["test"], Is.EqualTo("true")); } [Test] public void AddingOptionalBoolString_SetValueCorrectly() { - var parameters = new ParameterCollection(); - parameters.AddOptionalBoolString("test", true); + var parameters = new Parameters(new ParameterSerializationSettings() + { + Bool = BoolSerialization.String + }); + parameters.Add("test", true); Assert.That(parameters["test"], Is.EqualTo("true")); } [Test] public void AddingOptionalBoolStringNullValue_DoesntSetValue() { - var parameters = new ParameterCollection(); - parameters.AddOptionalBoolString("test", null); + var parameters = new Parameters(new ParameterSerializationSettings()); + parameters.Add("test", null); Assert.That(parameters.ContainsKey("test"), Is.False); } } diff --git a/CryptoExchange.Net.UnitTests/RateLimitTests.cs b/CryptoExchange.Net.UnitTests/RateLimitTests.cs index c2f2f854..a1c8a09c 100644 --- a/CryptoExchange.Net.UnitTests/RateLimitTests.cs +++ b/CryptoExchange.Net.UnitTests/RateLimitTests.cs @@ -29,16 +29,16 @@ namespace CryptoExchange.Net.UnitTests var triggered = false; rateLimiter.RateLimitTriggered += (x) => { triggered = true; }; - var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get); + var requestDefinition = new RequestDefinition("https://test.com", "/sapi/v1/system/status", HttpMethod.Get); for (var i = 0; i < requests + 1; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(i == requests ? triggered : !triggered); } triggered = false; await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(!triggered); } @@ -52,13 +52,13 @@ namespace CryptoExchange.Net.UnitTests var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); - var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get); + var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get); RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; for (var i = 0; i < 2; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null; Assert.That(expected); } @@ -73,15 +73,15 @@ namespace CryptoExchange.Net.UnitTests var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); - var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); - var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get); + var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get); + var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get); RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimiting ? evnt != null : evnt == null); } @@ -96,16 +96,16 @@ namespace CryptoExchange.Net.UnitTests bool triggered = false; rateLimiter.RateLimitTriggered += (x) => { triggered = true; }; - var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get); + var requestDefinition = new RequestDefinition("https://test.com", "/sapi/test", HttpMethod.Get); for (var i = 0; i < requests + 1; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(i == requests ? triggered : !triggered); } triggered = false; await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(!triggered); } @@ -117,13 +117,13 @@ namespace CryptoExchange.Net.UnitTests var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); - var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get); + var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get); RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; for (var i = 0; i < 2; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null; Assert.That(expected); } @@ -137,13 +137,13 @@ namespace CryptoExchange.Net.UnitTests { var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); - var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get); + var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get); RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; for (var i = 0; i < 2; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null; Assert.That(expected); } @@ -160,15 +160,15 @@ namespace CryptoExchange.Net.UnitTests { var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding)); - var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null }; - var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null }; + var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get) { Authenticated = key1 != null }; + var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = key2 != null }; RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -179,15 +179,15 @@ namespace CryptoExchange.Net.UnitTests { var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); - var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); - var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true }; + var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get); + var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = true }; RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -199,15 +199,15 @@ namespace CryptoExchange.Net.UnitTests { var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); - var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); - var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true }; + var requestDefinition1 = new RequestDefinition(host1, endpoint1, HttpMethod.Get); + var requestDefinition2 = new RequestDefinition(host2, endpoint2, HttpMethod.Get) { Authenticated = true }; RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -238,8 +238,8 @@ namespace CryptoExchange.Net.UnitTests rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); Assert.That(result2.Error, Is.TypeOf()); } @@ -250,16 +250,16 @@ namespace CryptoExchange.Net.UnitTests var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed)); - var definition = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 }; + var definition = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 }; RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); // act - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token); - await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, "https://test.com", null, null, default); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token); + await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token); // assert Assert.That(evnt, Is.Null); @@ -272,17 +272,17 @@ namespace CryptoExchange.Net.UnitTests var rateLimiter = new RateLimitGate("Test"); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed)); - var definition1 = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 }; - var definition2 = new RequestDefinition("2", HttpMethod.Get) { ConnectionId = 2 }; + var definition1 = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 }; + var definition2 = new RequestDefinition("https://test.com", "2", HttpMethod.Get) { ConnectionId = 2 }; RateLimitEvent? evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; // act - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default); - await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, "https://test.com", null, null, default); - var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default); + await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default); + var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default); // assert Assert.That(evnt, Is.Not.Null); diff --git a/CryptoExchange.Net.UnitTests/SocketRoutingTests/QueryRouterTests.cs b/CryptoExchange.Net.UnitTests/SocketRoutingTests/QueryRouterTests.cs index ce372193..23c99d93 100644 --- a/CryptoExchange.Net.UnitTests/SocketRoutingTests/QueryRouterTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketRoutingTests/QueryRouterTests.cs @@ -16,9 +16,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var routes = new MessageRoute[] { - MessageRoute.CreateWithoutTopicFilter("type1", (_, _, _, _) => null), - MessageRoute.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null), - MessageRoute.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null) + MessageRoute.CreateForEvent("type1", (_, _, _, _) => null), + MessageRoute.CreateForEvent("type1", "topic1", (_, _, _, _) => null), + MessageRoute.CreateForEvent("type2", "topic2", (_, _, _, _) => null) }; var router = new QueryRouter(routes); @@ -46,10 +46,10 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests var collection = new QueryRouteCollection(typeof(string)); // act - collection.AddRoute(null, MessageRoute.CreateWithoutTopicFilter("type", (_, _, _, _) => null)); + collection.AddRoute(null, MessageRoute.CreateForEvent("type", (_, _, _, _) => null)); var beforeMultipleReaders = collection.MultipleReaders; - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null, true)); + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => null, true)); var afterMultipleReaders = collection.MultipleReaders; // assert @@ -63,12 +63,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var calls = new List(); var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute(null, MessageRoute.CreateWithoutTopicFilter("type", (_, _, _, _) => + collection.AddRoute(null, MessageRoute.CreateForEvent("type", (_, _, _, _) => { calls.Add("no-topic"); return null; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("topic"); return null; @@ -89,7 +89,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests { // arrange var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute("other-topic", MessageRoute.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null)); + collection.AddRoute("other-topic", MessageRoute.CreateForEvent("type", "other-topic", (_, _, _, _) => null)); collection.Build(); // act @@ -106,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var calls = new List(); var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute(null, MessageRoute.CreateWithoutTopicFilter("type", (_, _, _, _) => + collection.AddRoute(null, MessageRoute.CreateForEvent("type", (_, _, _, _) => { calls.Add("no-topic"); return null; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("topic"); return null; @@ -132,17 +132,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests { // arrange var calls = new List(); - var expectedResult = CallResult.SuccessResult; + var expectedResult = CallResult.Ok(); var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("first"); return expectedResult; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("second"); - return new CallResult(null); + return CallResult.Ok(); })); collection.Build(); @@ -160,17 +160,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests { // arrange var calls = new List(); - var expectedResult = CallResult.SuccessResult; + var expectedResult = CallResult.Ok(); var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("first"); return expectedResult; }, true)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("second"); - return new CallResult(null); + return CallResult.Ok(); })); collection.Build(); @@ -188,22 +188,22 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests { // arrange var calls = new List(); - var expectedResult = CallResult.SuccessResult; + var expectedResult = CallResult.Ok(); var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("first"); return null; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("second"); return expectedResult; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("third"); - return new CallResult(null); + return CallResult.Ok(); })); collection.Build(); @@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests { // arrange var collection = new QueryRouteCollection(typeof(string)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null)); + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => null)); collection.Build(); // act diff --git a/CryptoExchange.Net.UnitTests/SocketRoutingTests/RoutingTableTests.cs b/CryptoExchange.Net.UnitTests/SocketRoutingTests/RoutingTableTests.cs index ccb6686e..9d3249c7 100644 --- a/CryptoExchange.Net.UnitTests/SocketRoutingTests/RoutingTableTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketRoutingTests/RoutingTableTests.cs @@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests var processor1 = new TestMessageProcessor( 1, MessageRouter.Create( - MessageRoute.CreateWithoutTopicFilter("type1", (_, _, _, _) => null), - MessageRoute.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type1", (_, _, _, _) => null), + MessageRoute.CreateForEvent("type1", "topic1", (_, _, _, _) => null))); var processor2 = new TestMessageProcessor( 2, MessageRouter.Create( - MessageRoute.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type2", "topic2", (_, _, _, _) => null))); var table = new RoutingTable(); @@ -57,12 +57,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests var processor1 = new TestMessageProcessor( 1, MessageRouter.Create( - MessageRoute.CreateWithoutTopicFilter("type1", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type1", (_, _, _, _) => null))); var processor2 = new TestMessageProcessor( 2, MessageRouter.Create( - MessageRoute.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type1", "topic1", (_, _, _, _) => null))); var table = new RoutingTable(); @@ -85,12 +85,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests var initialProcessor = new TestMessageProcessor( 1, MessageRouter.Create( - MessageRoute.CreateWithoutTopicFilter("type1", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type1", (_, _, _, _) => null))); var replacementProcessor = new TestMessageProcessor( 2, MessageRouter.Create( - MessageRoute.CreateWithoutTopicFilter("type2", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type2", (_, _, _, _) => null))); var table = new RoutingTable(); table.Update(new IMessageProcessor[] { initialProcessor }); @@ -116,7 +116,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests var processor = new TestMessageProcessor( 1, MessageRouter.Create( - MessageRoute.CreateWithoutTopicFilter("type1", (_, _, _, _) => null))); + MessageRoute.CreateForEvent("type1", (_, _, _, _) => null))); var table = new RoutingTable(); table.Update(new IMessageProcessor[] { processor }); diff --git a/CryptoExchange.Net.UnitTests/SocketRoutingTests/SubscriptionRouterTests.cs b/CryptoExchange.Net.UnitTests/SocketRoutingTests/SubscriptionRouterTests.cs index 8d688076..833c6119 100644 --- a/CryptoExchange.Net.UnitTests/SocketRoutingTests/SubscriptionRouterTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketRoutingTests/SubscriptionRouterTests.cs @@ -15,9 +15,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var routes = new MessageRoute[] { - MessageRoute.CreateWithoutTopicFilter("type1", (_, _, _, _) => null), - MessageRoute.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null), - MessageRoute.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null) + MessageRoute.CreateForEvent("type1", (_, _, _, _) => null), + MessageRoute.CreateForEvent("type1", "topic1", (_, _, _, _) => null), + MessageRoute.CreateForEvent("type2", "topic2", (_, _, _, _) => null) }; var router = new SubscriptionRouter(routes); @@ -44,12 +44,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var calls = new List(); var collection = new SubscriptionRouteCollection(typeof(string)); - collection.AddRoute(null, MessageRoute.CreateWithoutTopicFilter("type", (_, _, _, _) => + collection.AddRoute(null, MessageRoute.CreateForEvent("type", (_, _, _, _) => { calls.Add("no-topic"); return null; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("topic"); return null; @@ -61,7 +61,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // assert Assert.That(handled, Is.True); - Assert.That(result, Is.SameAs(CallResult.SuccessResult)); + Assert.That(result, Is.SameAs(CallResult.Ok())); Assert.That(calls, Is.EqualTo(new[] { "no-topic" })); } @@ -70,7 +70,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests { // arrange var collection = new SubscriptionRouteCollection(typeof(string)); - collection.AddRoute("other-topic", MessageRoute.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null)); + collection.AddRoute("other-topic", MessageRoute.CreateForEvent("type", "other-topic", (_, _, _, _) => null)); collection.Build(); // act @@ -78,7 +78,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // assert Assert.That(handled, Is.False); - Assert.That(result, Is.SameAs(CallResult.SuccessResult)); + Assert.That(result, Is.SameAs(CallResult.Ok())); } [Test] @@ -87,12 +87,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var calls = new List(); var collection = new SubscriptionRouteCollection(typeof(string)); - collection.AddRoute(null, MessageRoute.CreateWithoutTopicFilter("type", (_, _, _, _) => + collection.AddRoute(null, MessageRoute.CreateForEvent("type", (_, _, _, _) => { calls.Add("no-topic"); return null; })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("topic"); return null; @@ -104,7 +104,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // assert Assert.That(handled, Is.True); - Assert.That(result, Is.SameAs(CallResult.SuccessResult)); + Assert.That(result, Is.SameAs(CallResult.Ok())); Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" })); } @@ -114,12 +114,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var calls = new List(); var collection = new SubscriptionRouteCollection(typeof(string)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("first"); - return CallResult.SuccessResult; + return CallResult.Ok(); })); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("second"); return null; @@ -131,7 +131,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // assert Assert.That(handled, Is.True); - Assert.That(result, Is.SameAs(CallResult.SuccessResult)); + Assert.That(result, Is.SameAs(CallResult.Ok())); Assert.That(calls, Is.EqualTo(new[] { "first", "second" })); } @@ -141,7 +141,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // arrange var calls = new List(); var collection = new SubscriptionRouteCollection(typeof(string)); - collection.AddRoute("topic", MessageRoute.CreateWithTopicFilter("type", "topic", (_, _, _, _) => + collection.AddRoute("topic", MessageRoute.CreateForEvent("type", "topic", (_, _, _, _) => { calls.Add("topic"); return null; @@ -153,7 +153,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests // assert Assert.That(handled, Is.False); - Assert.That(result, Is.SameAs(CallResult.SuccessResult)); + Assert.That(result, Is.SameAs(CallResult.Ok())); Assert.That(calls, Is.Empty); } } diff --git a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs index 17ffd663..0ae58532 100644 --- a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs +++ b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs @@ -25,7 +25,7 @@ namespace CryptoExchange.Net.UnitTests } - protected override Task> DoResyncAsync(CancellationToken ct) + protected override Task DoResyncAsync(CancellationToken ct) { throw new NotImplementedException(); } diff --git a/CryptoExchange.Net.UnitTests/TokenManagementTests.cs b/CryptoExchange.Net.UnitTests/TokenManagementTests.cs new file mode 100644 index 00000000..21ba2149 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/TokenManagementTests.cs @@ -0,0 +1,347 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; +using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.Default; +using CryptoExchange.Net.TokenManagement; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; +using System; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture] + public class TokenManagementTests + { + private static readonly TimeSpan TestMaintenanceInterval = TimeSpan.FromMilliseconds(5); + + [Test] + public async Task AcquireWithoutApiKeyReturnsCredentialsError() + { + var starts = 0; + var manager = CreateManager( + (_, _) => + { + starts++; + return Task.FromResult(CallResult.Ok("token")); + }); + + var result = await manager.AcquireAsync(new TokenScope("Test", "Test", "Test", "")); + + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.TypeOf()); + Assert.That(starts, Is.EqualTo(0)); + } + + [Test] + public async Task StartTokenFailureIsReturned() + { + var error = new ServerError(ErrorType.Unknown, "start failed"); + var manager = CreateManager((_, _) => Task.FromResult(CallResult.Fail(error))); + + var result = await manager.AcquireAsync(CreateScope()); + + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.SameAs(error)); + } + + [Test] + public async Task ActiveTokenIsSharedWhileLeasedAndStoppedAfterLastRelease() + { + var starts = 0; + var stops = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + stopToken: (_, _) => + { + stops++; + return Task.FromResult(CallResult.Ok()); + }); + var scope = CreateScope(); + + var first = await manager.AcquireAsync(scope); + var second = await manager.AcquireAsync(scope); + AssertSuccess(first); + AssertSuccess(second); + + Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token)); + Assert.That(starts, Is.EqualTo(1)); + + await first.Data!.ReleaseAsync(); + Assert.That(stops, Is.EqualTo(0)); + + await second.Data!.ReleaseAsync(); + Assert.That(stops, Is.EqualTo(1)); + } + + [Test] + public async Task ActiveTokenStartsNewTokenAfterLeaseRelease() + { + var starts = 0; + var stops = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + stopToken: (_, _) => + { + stops++; + return Task.FromResult(CallResult.Ok()); + }); + var scope = CreateScope(); + + var first = await manager.AcquireAsync(scope); + AssertSuccess(first); + await first.Data!.ReleaseAsync(); + var second = await manager.AcquireAsync(scope); + AssertSuccess(second); + + Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token)); + Assert.That(starts, Is.EqualTo(2)); + Assert.That(stops, Is.EqualTo(1)); + await second.Data!.ReleaseAsync(); + } + + [Test] + public async Task ReleasingLeaseTwiceOnlyStopsActiveTokenOnce() + { + var stops = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token")), + stopToken: (_, _) => + { + stops++; + return Task.FromResult(CallResult.Ok()); + }); + + var leaseResult = await manager.AcquireAsync(CreateScope()); + AssertSuccess(leaseResult); + + await leaseResult.Data!.ReleaseAsync(); + await leaseResult.Data!.ReleaseAsync(); + + Assert.That(stops, Is.EqualTo(1)); + } + + [Test] + public async Task CachedTokenIsReusedAfterLeaseRelease() + { + var starts = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + managementType: TokenManagementType.Cached); + var scope = CreateScope(); + + var first = await manager.AcquireAsync(scope); + AssertSuccess(first); + await first.Data!.ReleaseAsync(); + var second = await manager.AcquireAsync(scope); + AssertSuccess(second); + + Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token)); + Assert.That(starts, Is.EqualTo(1)); + await second.Data!.ReleaseAsync(); + } + + [Test] + public async Task CachedTokensAreScopedIndependently() + { + var starts = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + managementType: TokenManagementType.Cached); + + var firstScope = CreateScope(additionalIdentifier: "one"); + var secondScope = CreateScope(additionalIdentifier: "two"); + + var first = await manager.AcquireAsync(firstScope); + var second = await manager.AcquireAsync(secondScope); + AssertSuccess(first); + AssertSuccess(second); + await first.Data!.ReleaseAsync(); + await second.Data!.ReleaseAsync(); + + var firstAgain = await manager.AcquireAsync(firstScope); + AssertSuccess(firstAgain); + + Assert.That(firstAgain.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token)); + Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token)); + Assert.That(starts, Is.EqualTo(2)); + await firstAgain.Data!.ReleaseAsync(); + } + + [Test] + public async Task ExpiredCachedTokenIsNotReused() + { + var starts = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + timeValid: TimeSpan.FromMilliseconds(20), + managementType: TokenManagementType.Cached); + var scope = CreateScope(); + + var first = await manager.AcquireAsync(scope); + AssertSuccess(first); + await first.Data!.ReleaseAsync(); + await Task.Delay(50); + var second = await manager.AcquireAsync(scope); + AssertSuccess(second); + + Assert.That(first.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired)); + Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token)); + Assert.That(starts, Is.EqualTo(2)); + await second.Data!.ReleaseAsync(); + } + + [Test] + public async Task CachedTokenDoesNotRunKeepAliveLoop() + { + var keepAlives = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token")), + refreshInterval: TimeSpan.FromMilliseconds(1), + keepAliveToken: (_, _) => + { + keepAlives++; + return Task.FromResult(CallResult.Ok()); + }, + managementType: TokenManagementType.Cached); + + var leaseResult = await manager.AcquireAsync(CreateScope()); + AssertSuccess(leaseResult); + await Task.Delay(50); + + Assert.That(keepAlives, Is.EqualTo(0)); + await leaseResult.Data!.ReleaseAsync(); + } + + [Test] + public async Task ActiveTokenKeepAliveRefreshesValidity() + { + var keepAlives = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token")), + refreshInterval: TimeSpan.FromMilliseconds(1), + timeValid: TimeSpan.FromSeconds(1), + keepAliveToken: (_, _) => + { + keepAlives++; + return Task.FromResult(CallResult.Ok()); + }); + + var leaseResult = await manager.AcquireAsync(CreateScope()); + AssertSuccess(leaseResult); + var originalValidUntil = leaseResult.Data!.Token.ValidUntil; + + await WaitUntilAsync(() => keepAlives > 0); + + Assert.That(leaseResult.Data!.Token.ValidUntil, Is.GreaterThan(originalValidUntil)); + await leaseResult.Data!.ReleaseAsync(); + } + + [Test] + public async Task ActiveTokenKeepAliveFailureExpiresTokenWhenValidityPassed() + { + var starts = 0; + var expired = false; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + refreshInterval: TimeSpan.FromMilliseconds(1), + timeValid: TimeSpan.FromMilliseconds(25), + keepAliveToken: (_, _) => Task.FromResult(CallResult.Fail(new ServerError(ErrorType.Unknown, "keep alive failed")))); + + var leaseResult = await manager.AcquireAsync(CreateScope()); + AssertSuccess(leaseResult); + leaseResult.Data!.Token.Expired += _ => expired = true; + + await WaitUntilAsync(() => expired); + + Assert.That(leaseResult.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired)); + + var nextLease = await manager.AcquireAsync(CreateScope()); + AssertSuccess(nextLease); + Assert.That(nextLease.Data!.Token.Token, Is.Not.EqualTo(leaseResult.Data!.Token.Token)); + Assert.That(starts, Is.EqualTo(2)); + + await leaseResult.Data!.ReleaseAsync(); + await nextLease.Data!.ReleaseAsync(); + } + + [Test] + public async Task AcquireAndReplaceReleasesPreviousSubscriptionLease() + { + var starts = 0; + var stops = 0; + var manager = CreateManager( + (_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)), + stopToken: (_, _) => + { + stops++; + return Task.FromResult(CallResult.Ok()); + }); + var subscription = new TestSubscription(); + + var first = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "one")); + AssertSuccess(first); + var second = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "two")); + AssertSuccess(second); + + Assert.That(subscription.TokenLease, Is.SameAs(second.Data)); + Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token)); + Assert.That(stops, Is.EqualTo(1)); + + await subscription.TokenLease!.ReleaseAsync(); + } + + private static TokenManager CreateManager( + Func>> startToken, + TimeSpan? refreshInterval = null, + TimeSpan? timeValid = null, + Func>? keepAliveToken = null, + Func>? stopToken = null, + TokenManagementType managementType = TokenManagementType.Active) + { + return new TokenManager( + Guid.NewGuid().ToString(), + null, + refreshInterval ?? TimeSpan.FromMinutes(1), + timeValid ?? TimeSpan.FromMinutes(1), + startToken, + keepAliveToken, + stopToken, + managementType, + TestMaintenanceInterval); + } + + private static TokenScope CreateScope(string apiKey = "apiKey", string? additionalIdentifier = null) + => new TokenScope("Test", "Test", "Test", apiKey, additionalIdentifier); + + private static void AssertSuccess(CallResult result) + { + Assert.That(result.Success, Is.True, result.Error?.ToString()); + Assert.That(result.Data, Is.Not.Null); + } + + private static async Task WaitUntilAsync(Func condition) + { + var timeout = DateTime.UtcNow.AddSeconds(2); + while (!condition()) + { + if (DateTime.UtcNow > timeout) + Assert.Fail("Condition was not met within the timeout"); + + await Task.Delay(10); + } + } + + private sealed class TestSubscription : Subscription + { + public TestSubscription() : base(NullLogger.Instance, true) + { + } + + protected override Query? GetSubQuery(SocketConnection connection) => null; + + protected override Query? GetUnsubQuery(SocketConnection connection) => null; + } + } +} diff --git a/CryptoExchange.Net/Attributes/NullableAttributes.cs b/CryptoExchange.Net/Attributes/NullableAttributes.cs index 156ac838..09b90b30 100644 --- a/CryptoExchange.Net/Attributes/NullableAttributes.cs +++ b/CryptoExchange.Net/Attributes/NullableAttributes.cs @@ -1,7 +1,7 @@ -#if NETSTANDARD2_0 -namespace System.Diagnostics.CodeAnalysis +namespace System.Diagnostics.CodeAnalysis { using System; +#if NETSTANDARD2_0 /// /// Specifies that is allowed as an input even if the @@ -206,5 +206,26 @@ namespace System.Diagnostics.CodeAnalysis ReturnValue = returnValue; } } +#endif +#if NETSTANDARD2_0 || NETSTANDARD2_1 + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + public bool ReturnValue { get; } + public string[] Members { get; } + } +#endif } -#endif \ No newline at end of file diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 20235358..fba89b32 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -439,13 +439,13 @@ namespace CryptoExchange.Net.Authentication /// /// /// - protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary parameters) + protected static string GetSerializedBody(IMessageSerializer serializer, Parameters? 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 stringSerializer.Serialize(value); + if (parameters?.BodyValue != null) + return stringSerializer.Serialize(parameters.BodyValue); else return stringSerializer.Serialize(parameters); } diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 717d9461..0d6e6b52 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace CryptoExchange.Net.Clients { @@ -25,7 +26,7 @@ namespace CryptoExchange.Net.Clients /// /// If we are disposing /// - protected bool _disposing; + protected bool _disposed; /// /// Whether a proxy is configured @@ -47,6 +48,11 @@ namespace CryptoExchange.Net.Clients } } + /// + /// The name of the exchange this client is for + /// + public string Exchange { get; } + /// /// The environment this client communicates to /// @@ -75,20 +81,26 @@ namespace CryptoExchange.Net.Clients /// /// ctor /// - /// Logger + /// Logger factory + /// The exchange name /// Should data from this client include the original data in the call result /// Base address for this API client /// Client options /// Api options protected BaseApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchange, bool outputOriginalData, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions) { - _logger = logger; + var loggerName = ClientName.StartsWith(exchange, StringComparison.OrdinalIgnoreCase) + ? exchange + "." + ClientName.Substring(exchange.Length).TrimStart('.') + : exchange + "." + ClientName; + _logger = loggerFactory?.CreateLogger(loggerName) ?? NullLogger.Instance; + Exchange = exchange; ClientOptions = clientOptions; ApiOptions = apiOptions; OutputOriginalData = outputOriginalData; @@ -113,9 +125,18 @@ namespace CryptoExchange.Net.Clients /// /// Dispose /// - public virtual void Dispose() + public void Dispose() { - _disposing = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose + /// + protected virtual void Dispose(bool disposing) + { + _disposed = true; } } } diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index 61cff2d4..064f0002 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -1,6 +1,7 @@ using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Threading; @@ -89,7 +90,6 @@ namespace CryptoExchange.Net.Clients throw new ArgumentNullException(nameof(options)); ClientOptions = options; - _logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}"); } /// @@ -115,6 +115,12 @@ namespace CryptoExchange.Net.Clients return opts; } + /// + public override string ToString() + { + return $"{GetType().Name}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}, configuration: {ClientOptions}"; + } + /// /// Dispose /// diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index f1a505ab..34b5148b 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -72,7 +72,7 @@ namespace CryptoExchange.Net.Clients /// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options. /// /// The credentials to set - public void SetApiCredentials(TApiCredentials credentials) + public virtual void SetApiCredentials(TApiCredentials credentials) { foreach (var apiClient in ApiClients) apiClient.SetApiCredentials(credentials); diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 6c438ded..44045c01 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -41,11 +41,6 @@ namespace CryptoExchange.Net.Clients /// protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json; - /// - /// How to serialize array parameters when making requests - /// - protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array; - /// /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) /// @@ -56,16 +51,6 @@ namespace CryptoExchange.Net.Clients /// protected Dictionary StandardRequestHeaders { get; set; } = []; - /// - /// Whether parameters need to be ordered - /// - protected internal bool OrderParameters { get; set; } = true; - - /// - /// Parameter order comparer - /// - protected IComparer ParameterOrderComparer { get; } = new OrderedStringComparer(); - /// /// Where to put the parameters for requests with different Http methods /// @@ -108,7 +93,7 @@ namespace CryptoExchange.Net.Clients /// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set /// public virtual AuthenticationProvider? GetAuthenticationProvider() => null; - + /// /// Configured environment name /// @@ -117,17 +102,20 @@ namespace CryptoExchange.Net.Clients /// /// ctor /// - /// Logger + /// Logger factory + /// The exchange name /// HttpClient to use /// Base address for this API client /// The base client options /// The Api client options - public RestApiClient(ILogger logger, + public RestApiClient(ILoggerFactory? loggerFactory, + string exchangeName, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) - : base(logger, + : base(loggerFactory, + exchangeName, apiOptions.OutputOriginalData ?? options.OutputOriginalData, baseAddress, options, @@ -144,33 +132,10 @@ namespace CryptoExchange.Net.Clients /// protected abstract IMessageSerializer CreateSerializer(); - /// - /// Send a request to the base address based on the request definition - /// - /// Host and schema - /// Request definition - /// Request parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// - protected virtual async Task SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? parameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null) - { - var result = await SendAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); - return result.AsDataless(); - } - /// /// Send a request to the base address based on the request definition /// /// Response type - /// Host and schema /// Request definition /// Request parameters /// Cancellation token @@ -179,10 +144,9 @@ namespace CryptoExchange.Net.Clients /// Specify the weight to apply to the individual rate limit guard for this request /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// - protected virtual Task> SendAsync( - string baseAddress, + protected virtual Task> SendAsync( RequestDefinition definition, - ParameterCollection? parameters, + Parameters? parameters, CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null, @@ -191,7 +155,6 @@ namespace CryptoExchange.Net.Clients { var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; return SendAsync( - baseAddress, definition, parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null, parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, @@ -206,7 +169,6 @@ namespace CryptoExchange.Net.Clients /// Send a request to the base address based on the request definition /// /// Response type - /// Host and schema /// Request definition /// Request query parameters /// Request body parameters @@ -216,11 +178,10 @@ namespace CryptoExchange.Net.Clients /// Specify the weight to apply to the individual rate limit guard for this request /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// - protected virtual async Task> SendAsync( - string baseAddress, + protected virtual async Task> SendAsync( RequestDefinition definition, - ParameterCollection? uriParameters, - ParameterCollection? bodyParameters, + Parameters? uriParameters, + Parameters? bodyParameters, CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null, @@ -231,20 +192,20 @@ namespace CryptoExchange.Net.Clients if (definition.Authenticated && GetAuthenticationProvider() == null) { _logger.RestApiNoApiCredentials(requestId, definition.Path); - return new WebCallResult(new NoApiCredentialsError()); + return HttpResult.Fail(Exchange, new NoApiCredentialsError()); } string? cacheKey = null; if (ShouldCache(definition)) { - cacheKey = baseAddress + definition + uriParameters?.ToFormData(); + cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData(); _logger.CheckingCache(cacheKey); var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); if (cachedValue != null) { _logger.CacheHit(cacheKey); - var original = (WebCallResult)cachedValue; - return original.Cached(); + var original = (HttpResult)cachedValue; + return original with { DataSource = ResultDataSource.Cache }; } _logger.CacheNotHit(cacheKey); @@ -258,7 +219,6 @@ namespace CryptoExchange.Net.Clients await CheckTimeSync(requestId, definition).ConfigureAwait(false); var error = await RateLimitAsync( - baseAddress, requestId, definition, weight ?? definition.Weight, @@ -266,11 +226,10 @@ namespace CryptoExchange.Net.Clients weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); if (error != null) - return new WebCallResult(error); + return HttpResult.Fail(Exchange, error); var request = CreateRequest( requestId, - baseAddress, definition, uriParameters, bodyParameters, @@ -284,7 +243,7 @@ namespace CryptoExchange.Net.Clients if (result.Error is not CancellationRequestedError) { var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; - if (!result) + if (!result.Success) { _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception); } @@ -316,7 +275,6 @@ namespace CryptoExchange.Net.Clients /// Check rate limits for the request /// protected virtual async ValueTask RateLimitAsync( - string host, int requestId, RequestDefinition definition, int weight, @@ -338,13 +296,12 @@ namespace CryptoExchange.Net.Clients requestId, RateLimitItemType.Request, definition, - host, GetAuthenticationProvider()?.Key, - requestWeight, + requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix + ClientOptions.RateLimitGroup, cancellationToken).ConfigureAwait(false); - if (!limitResult) + if (!limitResult.Success) return limitResult.Error!; } } @@ -360,17 +317,16 @@ namespace CryptoExchange.Net.Clients var singleRequestWeight = weightSingleLimiter ?? 1; var limitResult = await definition.RateLimitGate.ProcessSingleAsync( _logger, - requestId, + requestId, definition.LimitGuard, RateLimitItemType.Request, definition, - host, GetAuthenticationProvider()?.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); - if (!limitResult) + if (!limitResult.Success) return limitResult.Error!; } } @@ -382,7 +338,6 @@ namespace CryptoExchange.Net.Clients /// Creates a request object /// /// Id of the request - /// Host and schema /// Request definition /// The query parameters of the request /// The body parameters of the request @@ -390,19 +345,16 @@ namespace CryptoExchange.Net.Clients /// protected virtual IRequest CreateRequest( int requestId, - string baseAddress, RequestDefinition definition, - ParameterCollection? uriParameters, - ParameterCollection? bodyParameters, + Parameters? uriParameters, + Parameters? bodyParameters, Dictionary? additionalHeaders) { var requestConfiguration = new RestRequestConfiguration( definition, - baseAddress, - uriParameters == null ? null : CreateParameterDictionary(uriParameters), - bodyParameters == null ? null : CreateParameterDictionary(bodyParameters), + uriParameters, + bodyParameters, additionalHeaders, - definition.ArraySerialization ?? ArraySerialization, definition.ParameterPosition ?? ParameterPositions[definition.Method], definition.RequestBodyFormat ?? RequestBodyFormat); @@ -414,20 +366,16 @@ namespace CryptoExchange.Net.Clients { 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}"; - var path = baseAddress.AppendPath(definition.Path); - if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/")) - path += "/"; - - var uri = new Uri(path + queryString); + var uri = new Uri(definition.FullUrl + queryString); var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId); request.Accept = MessageHandler.AcceptHeader; - if (requestConfiguration.Headers != null) + if (requestConfiguration.Headers != null) { foreach (var header in requestConfiguration.Headers) request.AddHeader(header.Key, header.Value); @@ -436,10 +384,12 @@ namespace CryptoExchange.Net.Clients foreach (var header in StandardRequestHeaders) { // Only add it if it isn't overwritten - requestConfiguration.Headers ??= new Dictionary(); - if (!requestConfiguration.Headers.ContainsKey(header.Key)) + if (requestConfiguration.Headers == null + || !requestConfiguration.Headers.ContainsKey(header.Key)) + { request.AddHeader(header.Key, header.Value); - } + } + } if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody) { @@ -451,10 +401,10 @@ namespace CryptoExchange.Net.Clients } else { - if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) + if (requestConfiguration.BodyParameters != null && !requestConfiguration.BodyParameters.Empty) WriteParamBody(request, requestConfiguration.BodyParameters, contentType); else if (OmitContentTypeHeaderWithoutContent != true) - request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType); + request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType); } } @@ -469,7 +419,7 @@ namespace CryptoExchange.Net.Clients /// The ratelimit gate used /// Cancellation token /// - protected virtual async Task> GetResponseAsync2( + protected virtual async Task> GetResponseAsync2( RequestDefinition requestDefinition, IRequest request, IRateLimitGate? gate, @@ -535,16 +485,16 @@ namespace CryptoExchange.Net.Clients { _logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message); var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message }); - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult); + return FailHttpRequest(request, response, sw.Elapsed, originalData, errorResult); } } - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return FailHttpRequest(request, response, sw.Elapsed, originalData, error); } - if (typeof(T) == typeof(object)) + if (typeof(T) == Unit.Type) // Success status code and expected empty response, assume it's correct - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); + return OkHttpRequest(request, response, sw.Elapsed, originalData, default!); // Data response received, inspect the message and check if it is an error or not var parsedError = await MessageHandler.CheckForErrorResponse( @@ -563,7 +513,7 @@ namespace CryptoExchange.Net.Clients } // Success status code, but TryParseError determined it was an error response - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); + return FailHttpRequest(request, response, sw.Elapsed, originalData, parsedError); } if (MessageHandler.RequiresSeekableStream) @@ -571,45 +521,45 @@ namespace CryptoExchange.Net.Clients responseStream.Position = 0; // Try deserialization into the expected type - var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync(responseStream, cancellationToken).ConfigureAwait(false); + var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync(responseStream, cancellationToken).ConfigureAwait(false); if (deserializeError != null) - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ; + return FailHttpRequest(request, response, sw.Elapsed, originalData, deserializeError, deserializeResult); try { // Check the deserialized response to see if it's an error or not var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult); if (responseError != null) - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError); + return FailHttpRequest(request, response, sw.Elapsed, originalData, responseError, deserializeResult); } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message); var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message }); - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error); + return FailHttpRequest(request, response, sw.Elapsed, originalData, error, deserializeResult); } - return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null); + return OkHttpRequest(request, response, sw.Elapsed, originalData, deserializeResult!); } catch (HttpRequestException requestException) { // Request exception, can't reach server for instance var error = new WebError(requestException.Message, requestException); - return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return FailHttpRequest(request, response, sw.Elapsed, null, error); } catch (OperationCanceledException canceledException) { if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) { // Cancellation token canceled by caller - return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); + return FailHttpRequest(request, null, sw.Elapsed, null, new CancellationRequestedError(canceledException)); } else { // Request timed out var error = new WebError($"Request timed out", exception: canceledException); error.ErrorType = ErrorType.Timeout; - return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return FailHttpRequest(request, null, sw.Elapsed, null, error); } } catch (ArgumentException argumentException) @@ -618,7 +568,7 @@ namespace CryptoExchange.Net.Clients { // Unsupported HTTP version error .net framework var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message); - return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return FailHttpRequest(request, null, sw.Elapsed, null, error); } throw; @@ -629,7 +579,7 @@ namespace CryptoExchange.Net.Clients { // Unsupported HTTP version error dotnet code var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message); - return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return FailHttpRequest(request, null, sw.Elapsed, null, error); } throw; @@ -641,16 +591,55 @@ namespace CryptoExchange.Net.Clients } } + private HttpResult OkHttpRequest(IRequest request, IResponse response, TimeSpan elapsed, string? originalData, T result) + { + return HttpResult.Ok( + Exchange, + response.StatusCode, + response.HttpVersion, + response.ResponseHeaders, + elapsed, + response.ContentLength, + originalData, + request.RequestId, + request.Uri.ToString(), + request.Content, + request.Method, + request.GetHeaders(), + ResultDataSource.Server, + result); + } + + private HttpResult FailHttpRequest(IRequest request, IResponse? response, TimeSpan elapsed, string? originalData, Error error, T? result = default) + { + return HttpResult.Fail( + Exchange, + response?.StatusCode, + response?.HttpVersion, + response?.ResponseHeaders, + elapsed, + response?.ContentLength, + originalData, + request.RequestId, + request.Uri.ToString(), + request.Content, + request.Method, + request.GetHeaders(), + ResultDataSource.Server, + error, + result); + } + /// /// 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. /// Note that this is always called; even when the request might be successful /// - /// WebCallResult type parameter + /// HttpResult type parameter /// The rate limit gate the call used /// The result of the call /// The current try number /// True if call should retry, false if the call should return - protected virtual async ValueTask ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) + protected virtual async ValueTask ShouldRetryRequestAsync(IRateLimitGate? gate, HttpResult callResult, int tries) { if (tries >= 2) // Only retry once @@ -681,7 +670,7 @@ namespace CryptoExchange.Net.Clients /// The request to set the parameters on /// The parameters to set /// The content type of the data - protected virtual void WriteParamBody(IRequest request, IDictionary parameters, string contentType) + protected virtual void WriteParamBody(IRequest request, Parameters parameters, string contentType) { if (contentType == Constants.JsonContentHeader) { @@ -691,8 +680,13 @@ namespace CryptoExchange.Net.Clients // Write the parameters as json in the body string stringData; - if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) - stringData = stringSerializer.Serialize(value); + if (parameters.BodyValue != null) + { + if (parameters.BodyValue is string bodyString) + stringData = bodyString; + else + stringData = stringSerializer.Serialize(parameters.BodyValue); + } else stringData = stringSerializer.Serialize(parameters); request.SetContent(stringData, RequestBodyContentEncoding, contentType); @@ -705,24 +699,11 @@ namespace CryptoExchange.Net.Clients } } - /// - /// Create the parameter IDictionary - /// - /// - /// - protected internal IDictionary CreateParameterDictionary(IDictionary parameters) - { - if (!OrderParameters) - return parameters; - - return new SortedDictionary(parameters, ParameterOrderComparer); - } - /// /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues /// /// Server time - protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); + protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition) { @@ -757,7 +738,7 @@ namespace CryptoExchange.Net.Clients return; var localTime = DateTime.UtcNow; - WebCallResult result; + HttpResult result; try { result = await GetServerTimestampAsync().ConfigureAwait(false); @@ -767,7 +748,7 @@ namespace CryptoExchange.Net.Clients throw new ArgumentException("AutoTimestamp is not available for this API"); } - if (!result) + if (!result.Success) { _logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail"); return; @@ -778,7 +759,7 @@ namespace CryptoExchange.Net.Clients // If this was the first request make another one to calculate the offset since the first one can be slower localTime = DateTime.UtcNow; result = await GetServerTimestampAsync().ConfigureAwait(false); - if (!result) + if (!result.Success) { _logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail"); return; @@ -845,12 +826,14 @@ namespace CryptoExchange.Net.Clients /// ctor /// protected RestApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) : base( - logger, + loggerFactory, + exchangeName, httpClient, baseAddress, options, @@ -877,12 +860,14 @@ namespace CryptoExchange.Net.Clients /// ctor /// protected RestApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) : base( - logger, + loggerFactory, + exchangeName, httpClient, baseAddress, options, @@ -912,13 +897,18 @@ namespace CryptoExchange.Net.Clients where TAuthenticationProvider : AuthenticationProvider where TEnvironment : TradeEnvironment { - - private bool _authProviderInitialized = false; - private TAuthenticationProvider? _authenticationProvider; + /// + /// Auth provider initialized field + /// + protected bool _authProviderInitialized = false; + /// + /// Auth provider field + /// + protected TAuthenticationProvider? _authenticationProvider; /// /// The authentication provider for this API client. (null if no credentials are set) /// - public TAuthenticationProvider? AuthenticationProvider + public virtual TAuthenticationProvider? AuthenticationProvider { get { @@ -932,7 +922,7 @@ namespace CryptoExchange.Net.Clients return _authenticationProvider; } - internal set => _authenticationProvider = value; + protected internal set => _authenticationProvider = value; } /// @@ -942,12 +932,14 @@ namespace CryptoExchange.Net.Clients /// ctor /// protected RestApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) : base( - logger, + loggerFactory, + exchangeName, httpClient, baseAddress, options, diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 13449f87..d80def1d 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -15,6 +15,7 @@ using CryptoExchange.Net.Sockets.Default.Interfaces; using CryptoExchange.Net.Sockets.HighPerf; using CryptoExchange.Net.Sockets.HighPerf.Interfaces; using CryptoExchange.Net.Sockets.Interfaces; +using CryptoExchange.Net.TokenManagement; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; @@ -71,11 +72,6 @@ namespace CryptoExchange.Net.Clients /// protected List systemSubscriptions = new(); - /// - /// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message - /// - protected internal bool UnhandledMessageExpected { get; set; } - /// /// The rate limiters /// @@ -153,21 +149,26 @@ namespace CryptoExchange.Net.Clients /// Configured environment name /// public abstract string EnvironmentName { get; } + + private int _isDisposed; #endregion /// /// ctor /// - /// log + /// Logger factory + /// Exchange name /// Client options /// Base address for this API client /// The Api client options public SocketApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) - : base(logger, + : base(loggerFactory, + exchangeName, apiOptions.OutputOriginalData ?? options.OutputOriginalData, baseAddress, options, @@ -216,7 +217,7 @@ namespace CryptoExchange.Net.Clients /// /// /// - protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration { @@ -233,7 +234,7 @@ namespace CryptoExchange.Net.Clients /// The subscription /// Cancellation token for closing this subscription /// - protected virtual Task> SubscribeAsync(Subscription subscription, CancellationToken ct) + protected virtual Task> SubscribeAsync(Subscription subscription, CancellationToken ct) { return SubscribeAsync(BaseAddress, subscription, ct); } @@ -245,86 +246,102 @@ namespace CryptoExchange.Net.Clients /// The subscription /// Cancellation token for closing this subscription /// - protected virtual async Task> SubscribeAsync(string url, Subscription subscription, CancellationToken ct) + protected virtual async Task> SubscribeAsync(string url, Subscription subscription, CancellationToken ct) { - if (_disposing) - return new CallResult(new InvalidOperationError("Client disposed, can't subscribe")); - - if (subscription.Authenticated && GetAuthenticationProvider() == null) - { - _logger.LogWarning("Failed to subscribe, private subscription but no API credentials set"); - return new CallResult(new NoApiCredentialsError()); - } - - if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection) - return new CallResult(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}")); - - SocketConnection socketConnection; - var released = false; - // Wait for a semaphore here, so we only connect 1 socket at a time. - // This is necessary for being able to see if connections can be combined + bool successResult = false; try { - await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); - } - catch (OperationCanceledException tce) - { - return new CallResult(new CancellationRequestedError(tce)); - } + if (_disposed) + return WebSocketResult.Fail(Exchange, new InvalidOperationError("Client disposed, can't subscribe")); - try - { - while (true) + if (subscription.Authenticated && GetAuthenticationProvider() == null) { - // Get a new or existing socket connection - var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false); - if (!socketResult) - return socketResult.As(null); - - socketConnection = socketResult.Data; - - // Add a subscription on the socket connection - var success = socketConnection.AddSubscription(subscription); - if (!success) - { - _logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId); - continue; - } - - if (ClientOptions.SocketSubscriptionsCombineTarget == 1) - { - // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway - semaphoreSlim.Release(); - released = true; - } - - var needsConnecting = !socketConnection.Connected; - - var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false); - if (!connectResult) - return new CallResult(connectResult.Error!); - - break; + _logger.LogWarning("Failed to subscribe, private subscription but no API credentials set"); + return WebSocketResult.Fail(Exchange, new NoApiCredentialsError()); } + + if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection) + return WebSocketResult.Fail(Exchange, ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}")); + + SocketConnection socketConnection; + var released = false; + // Wait for a semaphore here, so we only connect 1 socket at a time. + // This is necessary for being able to see if connections can be combined + try + { + await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException tce) + { + return WebSocketResult.Fail(Exchange, new CancellationRequestedError(tce)); + } + + try + { + while (true) + { + // Get a new or existing socket connection + var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false); + if (!socketResult.Success) + return WebSocketResult.Fail(Exchange, socketResult.Error); + + socketConnection = socketResult.Data; + + // Add a subscription on the socket connection + var success = socketConnection.AddSubscription(subscription); + if (!success) + { + _logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId); + continue; + } + + if (ClientOptions.SocketSubscriptionsCombineTarget == 1) + { + // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway + semaphoreSlim.Release(); + released = true; + } + + var needsConnecting = !socketConnection.Connected; + + var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false); + if (!connectResult.Success) + return WebSocketResult.Fail(Exchange, connectResult.Error!); + + break; + } + } + finally + { + if (!released) + semaphoreSlim.Release(); + } + + if (socketConnection.PausedActivity) + { + _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); + return WebSocketResult.Fail(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); + } + + var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false); + if (!subscribeResult.Success) + return WebSocketResult.Fail(Exchange, subscribeResult.Error!); + + successResult = true; + _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); + return WebSocketResult.Ok( + Exchange, + socketConnection.SocketId, + subscribeResult.ResponseTime!.Value, + subscribeResult.RequestId!.Value, + subscribeResult.Url, + new UpdateSubscription(socketConnection, subscription)); } finally { - if (!released) - semaphoreSlim.Release(); + if (!successResult && subscription.TokenLease != null) + _ = subscription.TokenLease.ReleaseAsync(); } - - if (socketConnection.PausedActivity) - { - _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); - } - - var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false); - if (!subscribeResult) - return new CallResult(subscribeResult.Error!); - - _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); - return new CallResult(new UpdateSubscription(socketConnection, subscription)); } /// @@ -335,14 +352,14 @@ namespace CryptoExchange.Net.Clients /// The factory for creating a socket connection /// Cancellation token for closing this subscription /// - protected virtual async Task> SubscribeHighPerfAsync( + protected virtual async Task> SubscribeHighPerfAsync( string url, HighPerfSubscription subscription, IHighPerfConnectionFactory connectionFactory, CancellationToken ct) { - if (_disposing) - return new CallResult(new InvalidOperationError("Client disposed, can't subscribe")); + if (_disposed) + return WebSocketResult.Fail(Exchange, new InvalidOperationError("Client disposed, can't subscribe")); HighPerfSocketConnection socketConnection; var released = false; @@ -354,7 +371,7 @@ namespace CryptoExchange.Net.Clients } catch (OperationCanceledException tce) { - return new CallResult(new CancellationRequestedError(tce)); + return WebSocketResult.Fail(Exchange, new CancellationRequestedError(tce)); } try @@ -363,8 +380,8 @@ namespace CryptoExchange.Net.Clients { // Get a new or existing socket connection var socketResult = await GetHighPerfSocketConnection(url, connectionFactory, ct).ConfigureAwait(false); - if (!socketResult) - return socketResult.As(null); + if (!socketResult.Success) + return WebSocketResult.Fail(Exchange, socketResult.Error); socketConnection = socketResult.Data; @@ -384,8 +401,8 @@ namespace CryptoExchange.Net.Clients } var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false); - if (!connectResult) - return new CallResult(connectResult.Error!); + if (!connectResult.Success) + return WebSocketResult.Fail(Exchange, connectResult.Error!); break; } @@ -401,10 +418,10 @@ namespace CryptoExchange.Net.Clients { // Send the request and wait for answer var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false); - if (!sendResult) + if (!sendResult.Success) { await socketConnection.CloseAsync().ConfigureAwait(false); - return new CallResult(sendResult.Error!); + return WebSocketResult.Fail(Exchange, sendResult.Error!); } } @@ -418,7 +435,13 @@ namespace CryptoExchange.Net.Clients } _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); - return new CallResult(new HighPerfUpdateSubscription(socketConnection, subscription)); + return WebSocketResult.Ok( + Exchange, + socketConnection.SocketId, + default, + default, + socketConnection.ConnectionUri.ToString(), + new HighPerfUpdateSubscription(socketConnection, subscription)); } /// @@ -428,7 +451,7 @@ namespace CryptoExchange.Net.Clients /// The query /// Cancellation token /// - protected virtual Task> QueryAsync(Query query, CancellationToken ct = default) + protected virtual Task> QueryAsync(Query query, CancellationToken ct = default) { return QueryAsync(BaseAddress, query, ct); } @@ -441,13 +464,13 @@ namespace CryptoExchange.Net.Clients /// The query /// Cancellation token /// - protected virtual async Task> QueryAsync(string url, Query query, CancellationToken ct = default) + protected virtual async Task> QueryAsync(string url, Query query, CancellationToken ct = default) { - if (_disposing) - return new CallResult(new InvalidOperationError("Client disposed, can't query")); + if (_disposed) + return QueryResult.Fail(Exchange, new InvalidOperationError("Client disposed, can't query")); if (ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); + return QueryResult.Fail(Exchange, new CancellationRequestedError()); SocketConnection socketConnection; var released = false; @@ -455,8 +478,8 @@ namespace CryptoExchange.Net.Clients try { var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false); - if (!socketResult) - return socketResult.As(default); + if (!socketResult.Success) + return QueryResult.Fail(Exchange, socketResult.Error); socketConnection = socketResult.Data; @@ -468,8 +491,8 @@ namespace CryptoExchange.Net.Clients } var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false); - if (!connectResult) - return new CallResult(connectResult.Error!); + if (!connectResult.Success) + return QueryResult.Fail(Exchange, connectResult.Error!); } finally { @@ -480,11 +503,11 @@ namespace CryptoExchange.Net.Clients if (socketConnection.PausedActivity) { _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); + return QueryResult.Fail(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); } if (ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); + return QueryResult.Fail(Exchange, new CancellationRequestedError()); return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false); } @@ -499,23 +522,23 @@ namespace CryptoExchange.Net.Clients protected virtual async Task ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct) { if (socket.Connected) - return CallResult.SuccessResult; + return CallResult.Ok(); var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false); - if (!connectResult) + if (!connectResult.Success) return connectResult; if (ClientOptions.DelayAfterConnect != TimeSpan.Zero) await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false); if (!authenticated || socket.Authenticated) - return CallResult.SuccessResult; + return CallResult.Ok(); if (socket is not SocketConnection sc) throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication"); var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false); - if (!result) + if (!result.Success) await socket.CloseAsync().ConfigureAwait(false); return result; @@ -529,29 +552,28 @@ namespace CryptoExchange.Net.Clients public virtual async Task AuthenticateSocketAsync(SocketConnection socket) { if (GetAuthenticationProvider() == null) - return new CallResult(new NoApiCredentialsError()); + return CallResult.Fail(new NoApiCredentialsError()); _logger.AttemptingToAuthenticate(socket.SocketId); var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false); if (authRequest != null) { var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); - - if (!result) + if (!result.Success) { _logger.AuthenticationFailed(socket.SocketId); if (socket.Connected) await socket.CloseAsync().ConfigureAwait(false); result.Error!.Message = "Authentication failed: " + result.Error.Message; - return new CallResult(result.Error)!; + return CallResult.Fail(result.Error)!; } _logger.Authenticated(socket.SocketId); } socket.Authenticated = true; - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -580,7 +602,7 @@ namespace CryptoExchange.Net.Clients /// protected virtual Task> GetConnectionUrlAsync(string address, bool authentication) { - return Task.FromResult(new CallResult(address)); + return Task.FromResult(CallResult.Ok(address)); } /// @@ -600,7 +622,7 @@ namespace CryptoExchange.Net.Clients /// protected internal virtual Task RevitalizeRequestAsync(Subscription subscription) { - return Task.FromResult(CallResult.SuccessResult); + return Task.FromResult(CallResult.Ok()); } /// @@ -621,24 +643,23 @@ namespace CryptoExchange.Net.Clients string? topic = null, int individualSubscriptionCount = 1) { - var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') - && s.Value.ApiClient.GetType() == GetType() - && (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))) - .Select(x => x.Value) - .ToList(); + var socketQuery = _socketConnections.Where(s => s.Value.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal) + && s.Value.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal) + && (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))) + .Select(x => x.Value); // Don't ToList this so the query is executed again when called // 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 == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing)) + while (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing)) { if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10)) { - if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing)) + if (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing)) { // If after this time we still trying to reconnect/reprocess there is some issue in the connection _logger.TimeoutWaitingForReconnectingSocket(); - return new CallResult(new CantConnectError()); + return CallResult.Fail(new CantConnectError()); } break; @@ -648,7 +669,7 @@ namespace CryptoExchange.Net.Clients try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { } if (ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); + return CallResult.Fail(new CancellationRequestedError()); } if (delayed) @@ -661,7 +682,10 @@ namespace CryptoExchange.Net.Clients SocketConnection? connection; if (!dedicatedRequestConnection) { - connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault(); + connection = socketQuery + .Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection) + .OrderBy(s => s.UserSubscriptionCount) + .FirstOrDefault(); } else { @@ -687,29 +711,29 @@ namespace CryptoExchange.Net.Clients // Use existing socket if it has less than target connections OR it has the least connections and we can't make new // If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit if (MaxIndividualSubscriptionsPerConnection == null) - return new CallResult(connection); + return CallResult.Ok(connection); var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount); if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection) - return new CallResult(connection); + return CallResult.Ok(connection); } } if (maxConnectionsReached) - return new CallResult(new InvalidOperationError("Max amount of socket connections reached")); + return CallResult.Fail(new InvalidOperationError("Max amount of socket connections reached")); var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); - if (!connectionAddress) + if (!connectionAddress.Success) { - _logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); - return connectionAddress.As(null); + _logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString()); + return CallResult.Fail(connectionAddress.Error); } if (connectionAddress.Data != address) _logger.ConnectionAddressSetTo(connectionAddress.Data!); // Create new socket connection - var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address); + var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this); socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync; if (dedicatedRequestConnection) { @@ -726,7 +750,7 @@ namespace CryptoExchange.Net.Clients foreach (var systemSubscription in systemSubscriptions) socketConnection.AddSubscription(systemSubscription); - return new CallResult(socketConnection); + return CallResult.Ok(socketConnection); } @@ -743,21 +767,21 @@ namespace CryptoExchange.Net.Clients CancellationToken ct) { var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false); - if (!connectionAddress) + if (!connectionAddress.Success) { - _logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); - return connectionAddress.As>(null); + _logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString()); + return CallResult.Fail>(connectionAddress.Error); } if (connectionAddress.Data != address) _logger.ConnectionAddressSetTo(connectionAddress.Data!); // Create new socket connection - var socketConnection = connectionFactory.CreateHighPerfConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address); + var socketConnection = connectionFactory.CreateHighPerfConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this); foreach (var ptg in PeriodicTaskRegistrations) socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request); - return new CallResult>(socketConnection); + return CallResult.Ok(socketConnection); } /// @@ -791,7 +815,7 @@ namespace CryptoExchange.Net.Clients protected virtual async Task ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct) { var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false); - if (connectResult) + if (connectResult.Success) { if (socketConnection is SocketConnection sc) _socketConnections.TryAdd(socketConnection.SocketId, sc); @@ -875,7 +899,7 @@ namespace CryptoExchange.Net.Clients _logger.UnsubscribingAll(sum); var tasks = new List(); - + var socketList = _socketConnections.Values; foreach (var connection in socketList) { @@ -914,15 +938,15 @@ namespace CryptoExchange.Net.Clients foreach (var item in DedicatedConnectionConfigs) { var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false); - if (!socketResult) - return socketResult.AsDataless(); + if (!socketResult.Success) + return CallResult.Fail(socketResult.Error); var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false); - if (!connectResult) - return new CallResult(connectResult.Error!); + if (!connectResult.Success) + return CallResult.Fail(connectResult.Error!); } - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -1004,23 +1028,28 @@ namespace CryptoExchange.Net.Clients /// /// Dispose the client /// - public override void Dispose() + protected override void Dispose(bool disposing) { - _disposing = true; - var tasks = new List(); + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) { - var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); - if (socketList.Any()) - _logger.DisposingSocketClient(); + if (!disposing) + return; - foreach (var connection in socketList) + var tasks = new List(); { - tasks.Add(connection.CloseAsync()); - } - } + var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); + if (socketList.Any()) + _logger.DisposingSocketClient(); - semaphoreSlim?.Dispose(); - base.Dispose(); + foreach (var connection in socketList) + { + tasks.Add(connection.CloseAsync()); + } + } + + semaphoreSlim?.Dispose(); + base.Dispose(disposing); + } } /// @@ -1071,11 +1100,13 @@ namespace CryptoExchange.Net.Clients /// ctor /// protected SocketApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) : base( - logger, + loggerFactory, + exchangeName, baseAddress, options, apiOptions) @@ -1101,11 +1132,13 @@ namespace CryptoExchange.Net.Clients /// ctor /// protected SocketApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) : base( - logger, + loggerFactory, + exchangeName, baseAddress, options, apiOptions) @@ -1132,13 +1165,18 @@ namespace CryptoExchange.Net.Clients where TApiCredentials : ApiCredentials where TEnvironment : TradeEnvironment { - - private bool _authProviderInitialized = false; - private TAuthenticationProvider? _authenticationProvider; + /// + /// Auth provider initialized field + /// + protected bool _authProviderInitialized = false; + /// + /// Auth provider field + /// + protected TAuthenticationProvider? _authenticationProvider; /// /// The authentication provider for this API client. (null if no credentials are set) /// - public TAuthenticationProvider? AuthenticationProvider + public virtual TAuthenticationProvider? AuthenticationProvider { get { @@ -1152,7 +1190,7 @@ namespace CryptoExchange.Net.Clients return _authenticationProvider; } - internal set => _authenticationProvider = value; + protected internal set => _authenticationProvider = value; } /// @@ -1162,11 +1200,13 @@ namespace CryptoExchange.Net.Clients /// ctor /// protected SocketApiClient( - ILogger logger, + ILoggerFactory? loggerFactory, + string exchangeName, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) : base( - logger, + loggerFactory, + exchangeName, baseAddress, options, apiOptions) diff --git a/CryptoExchange.Net/Clients/UserClientProvider.cs b/CryptoExchange.Net/Clients/UserClientProvider.cs new file mode 100644 index 00000000..c6cb2232 --- /dev/null +++ b/CryptoExchange.Net/Clients/UserClientProvider.cs @@ -0,0 +1,172 @@ +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces.Clients; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Options; +using CryptoExchange.Net.SharedApis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; + +namespace CryptoExchange.Net.Clients +{ + /// + public abstract class UserClientProvider + where TRestClient : IRestClient + where TSocketClient : ISocketClient + where TRestOptions : RestExchangeOptions, new() + where TSocketOptions : SocketExchangeOptions, new() + where TCredentials : ApiCredentials + where TEnvironment : TradeEnvironment + { + private ConcurrentDictionary _restClients = new ConcurrentDictionary(); + private ConcurrentDictionary _socketClients = new ConcurrentDictionary(); + + private readonly IOptions _restOptions; + private readonly IOptions _socketOptions; + private readonly HttpClient _httpClient; + private readonly ILoggerFactory? _loggerFactory; + + /// + public abstract string ExchangeName { get; } + + /// + /// ctor + /// + public UserClientProvider( + HttpClient? httpClient, + ILoggerFactory? loggerFactory, + IOptions restOptions, + IOptions socketOptions) + { + _httpClient = httpClient ?? new HttpClient(); + _httpClient.Timeout = restOptions.Value.RequestTimeout; + _loggerFactory = loggerFactory; + _restOptions = restOptions; + _socketOptions = socketOptions; + } + + + private IOptions SetRestEnvironment(IOptions options, TEnvironment? environment) + { + if (environment == null) + return options; + + var newRestClientOptions = new TRestOptions(); + options.Value.Set(newRestClientOptions); + newRestClientOptions.Environment = environment; + return Options.Create(newRestClientOptions); + } + + private IOptions SetSocketEnvironment(IOptions options, TEnvironment? environment) + { + if (environment == null) + return options; + + var newSocketClientOptions = new TSocketOptions(); + options.Value.Set(newSocketClientOptions); + newSocketClientOptions.Environment = environment; + return Options.Create(newSocketClientOptions); + } + + /// + public void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null) + { + CreateRestClient(userIdentifier, credentials, environment); + CreateSocketClient(userIdentifier, credentials, environment); + } + + /// + public TRestClient GetRestClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null) + { + if (!_restClients.TryGetValue(userIdentifier, out var client) || client.Disposed) + client = CreateRestClient(userIdentifier, credentials, environment); + + return client; + } + + /// + public TSocketClient GetSocketClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null) + { + if (!_socketClients.TryGetValue(userIdentifier, out var client) || client.Disposed) + client = CreateSocketClient(userIdentifier, credentials, environment); + + return client; + } + + private TRestClient CreateRestClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment) + { + var clientRestOptions = SetRestEnvironment(_restOptions, environment); + var client = ConstructRestClient(_httpClient, _loggerFactory, clientRestOptions); + if (credentials != null) + { + _restClients[userIdentifier] = client; + client.SetApiCredentials(credentials); + } + return client; + } + + private TSocketClient CreateSocketClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment) + { + var clientSocketOptions = SetSocketEnvironment(_socketOptions, environment); + var client = ConstructSocketClient(_loggerFactory, clientSocketOptions); + if (credentials != null) + { + _socketClients[userIdentifier] = client; + client.SetApiCredentials(credentials); + } + return client; + } + + /// + /// Constructs a new instance of the rest client + /// + protected abstract TRestClient ConstructRestClient( + HttpClient client, + ILoggerFactory? loggerFactory, + IOptions options); + + + /// + /// Constructs a new instance of the socket client + /// + protected abstract TSocketClient ConstructSocketClient( + ILoggerFactory? loggerFactory, + IOptions options); + + + /// + public void ClearUserClients(string userIdentifier) + { + _restClients.TryRemove(userIdentifier, out var restClient); + _socketClients.TryRemove(userIdentifier, out var socketClient); + restClient?.Dispose(); + socketClient?.Dispose(); + } + + /// + public void Clear() + { + foreach (var client in _restClients.Values) + client.Dispose(); + _restClients.Clear(); + + foreach (var client in _socketClients.Values) + client.Dispose(); + _socketClients.Clear(); + } + + /// + /// Applies the provided options delegate to a new instance of the specified type. + /// + protected static T ApplyOptionsDelegate(Action? del) where T : new() + { + var opts = new T(); + del?.Invoke(opts); + return opts; + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs index 775ddde9..9309f777 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs @@ -56,14 +56,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (reader.TokenType == JsonTokenType.False) return false; - var value = reader.TokenType switch + if (reader.TokenType == JsonTokenType.Number) { - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetInt16().ToString(), - _ => null - }; + var number = reader.GetInt16(); + if (number > 1) + return true; - value = value?.ToLowerInvariant().Trim(); + return false; + } + + if (reader.TokenType == JsonTokenType.Null) + { + if (typeToConvert == typeof(bool)) + LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); + return default; + } + + if (reader.TokenType != JsonTokenType.String) + throw new SerializationException($"Can't convert bool value for token type {reader.TokenType}"); + + var value = reader.GetString()?.ToLowerInvariant().Trim(); if (string.IsNullOrEmpty(value)) { if (typeToConvert == typeof(bool)) @@ -73,12 +85,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson switch (value) { + case "enabled": case "true": case "yes": case "y": case "1": case "on": return true; + case "disabled": case "false": case "no": case "n": @@ -88,7 +102,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return false; } - throw new SerializationException($"Can't convert bool value {value}"); + throw new SerializationException($"Can't convert bool value, unknown string value: {value}"); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs index 357c02e8..d76b255b 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -16,17 +16,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m; private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; + private static Type _dateTimeType = typeof(DateTime); + private static Type _nullableDateTimeType = typeof(DateTime?); /// public override bool CanConvert(Type typeToConvert) { - return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?); + return typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType; } /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner(); + return typeToConvert == _dateTimeType ? new DateTimeConverterInner() : new NullableDateTimeConverterInner(); } private class NullableDateTimeConverterInner : JsonConverter @@ -68,7 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { if (reader.TokenType == JsonTokenType.Null) { - if (typeToConvert == typeof(DateTime)) + if (typeToConvert == _dateTimeType) LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); return default; } @@ -76,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (reader.TokenType is JsonTokenType.Number) { var decValue = reader.GetDecimal(); - if (decValue == 0 || decValue < 0) + if (decValue <= 0) return default; return ParseFromDecimal(decValue); @@ -86,8 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson var stringValue = reader.GetString(); if (string.IsNullOrWhiteSpace(stringValue) || stringValue!.Equals("-1", StringComparison.Ordinal) - || stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase) - || decimal.TryParse(stringValue, out var decVal) && decVal == 0) + || stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)) { return default; } @@ -124,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// /// Parse a string value to datetime /// - public static DateTime ParseFromString(string stringValue, string? resolverName) + public static DateTime? ParseFromString(string stringValue, string? resolverName) { if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase)) { diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 9add5607..130127d6 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -6,9 +6,9 @@ CryptoExchange.Net JKorf 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. - 11.2.2 - 11.2.2 - 11.2.2 + 12.0.0-beta1 + 12.0.0 + 12.0.0 false 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 git diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs index 98613cbf..fcc3e8c4 100644 --- a/CryptoExchange.Net/ExchangeHelpers.cs +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -311,16 +311,16 @@ namespace CryptoExchange.Net /// The request parameters /// Cancellation token /// - public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) + public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) { var result = new List(); - ExchangeWebResult batch; + HttpResult batch; PageRequest? nextPageToken = null; while (true) { batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false); yield return batch; - if (!batch || ct.IsCancellationRequested) + if (!batch.Success || ct.IsCancellationRequested) break; result.AddRange(batch.Data); @@ -399,8 +399,8 @@ namespace CryptoExchange.Net /// The async update handler /// The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with fullMode /// What should happen if the queue contains maxQueuedItems pending updates. If no max is set this setting is ignored - public static async Task> ProcessQueuedAsync( - Func>, Task>> subscribeCall, + public static async Task> ProcessQueuedAsync( + Func>, Task>> subscribeCall, Func, Task> asyncHandler, int? maxQueuedItems = null, QueueFullBehavior? fullBehavior = null) @@ -408,7 +408,7 @@ namespace CryptoExchange.Net var processor = new ProcessQueue>(asyncHandler, maxQueuedItems, fullBehavior); await processor.StartAsync().ConfigureAwait(false); var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false); - if (!result) + if (!result.Success) { await processor.StopAsync().ConfigureAwait(false); return result; @@ -473,7 +473,7 @@ namespace CryptoExchange.Net }, maxQueuedItems, fullBehavior); await processor.StartAsync().ConfigureAwait(false); var result = await subscribeCall(processor).ConfigureAwait(false); - if (!result) + if (!result.Success) { await processor.StopAsync().ConfigureAwait(false); return result; @@ -499,7 +499,7 @@ namespace CryptoExchange.Net return null; // Try parse, only fails for these reasons: - // 1. string is null or empty + // 1. string is null or empty (already covered) // 2. value is larger or smaller than decimal max/min // 3. unparsable format if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue)) @@ -516,7 +516,7 @@ namespace CryptoExchange.Net if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase)) return decimal.MaxValue; else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase)) - return decimal.MinValue; + return decimal.MinValue; if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue)) { diff --git a/CryptoExchange.Net/ExchangeSymbolCache.cs b/CryptoExchange.Net/ExchangeSymbolCache.cs index da007293..e3b0fc8b 100644 --- a/CryptoExchange.Net/ExchangeSymbolCache.cs +++ b/CryptoExchange.Net/ExchangeSymbolCache.cs @@ -11,85 +11,92 @@ namespace CryptoExchange.Net /// public static class ExchangeSymbolCache { - private static ConcurrentDictionary _symbolInfos = new ConcurrentDictionary(); + private static ConcurrentDictionary _symbolInfos = new ConcurrentDictionary(); /// /// Update the cached symbol data for an exchange /// /// Id for the provided data + /// Trading environment + /// Optional data set key /// Symbol data - public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData) + public static void UpdateSymbolInfo(string topicId, string environment, string? key, SharedSpotSymbol[] updateData) { - if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + var id = topicId + environment; + if(!_symbolInfos.TryGetValue(id, out var exchangeInfo)) { - exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)); - _symbolInfos.TryAdd(topicId, exchangeInfo); + exchangeInfo = new ExchangeKeyedCache(); + _symbolInfos.TryAdd(id, exchangeInfo); } - if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60)) + var keyedCache = exchangeInfo.Get(key); + if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60)) return; - _symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)); + exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol))); } /// /// Whether the specific topic has been cached /// /// Id - public static bool HasCached(string topicId) + /// Trading environment + /// Optional data set key + public static bool HasCached(string topicId, string environment, string? key) { - if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + var id = topicId + environment; + if (!_symbolInfos.TryGetValue(id, out var exchangeInfo)) return false; - return exchangeInfo.Symbols.Count > 0; + return exchangeInfo.HasCached(key); } /// /// Whether a specific exchange(topic) support the provided symbol /// /// Id for the provided data + /// Trading environment + /// Optional data set key /// The symbol name - public static bool SupportsSymbol(string topicId, string symbolName) + public static bool SupportsSymbol(string topicId, string environment, string? key, string symbolName) { - if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + var id = topicId + environment; + if (!_symbolInfos.TryGetValue(id, out var exchangeInfo)) return false; - if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo)) - return false; - - return true; + return exchangeInfo.SupportsSymbol(key, symbolName); } /// /// Whether a specific exchange(topic) support the provided symbol /// /// Id for the provided data + /// Trading environment + /// Optional data set key /// The symbol info - public static bool SupportsSymbol(string topicId, SharedSymbol symbol) + public static bool SupportsSymbol(string topicId, string environment, string? key, SharedSymbol symbol) { - if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + var id = topicId + environment; + if (!_symbolInfos.TryGetValue(id, out var exchangeInfo)) return false; - return exchangeInfo.Symbols.Any(x => - x.Value.TradingMode == symbol.TradingMode - && x.Value.BaseAsset == symbol.BaseAsset - && x.Value.QuoteAsset == symbol.QuoteAsset); + return exchangeInfo.SupportsSymbol(key, symbol); } /// /// Get all symbols for a specific base asset /// /// Id for the provided data + /// Trading environment + /// Optional data set key /// Base asset name - public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset) + public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string environment, string? key, string baseAsset) { - if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + var id = topicId + environment; + if (!_symbolInfos.TryGetValue(id, out var exchangeInfo)) return []; - return exchangeInfo.Symbols - .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) - .Select(x => x.Value) - .ToArray(); + return exchangeInfo.GetSymbolsForBaseAsset(key, baseAsset); } /// @@ -97,23 +104,194 @@ namespace CryptoExchange.Net /// /// Id for the provided data /// Symbol name - public static SharedSymbol? ParseSymbol(string topicId, string? symbolName) + /// Trade environment + /// Additional data set identification key + public static SharedSymbol? ParseSymbol(string topicId, string environment, string? key, string? symbolName) { if (symbolName == null) return null; - if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + var id = topicId + environment; + if (!_symbolInfos.TryGetValue(id, out var exchangeInfo)) return null; - if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo)) - return null; - - return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName) - { - DeliverTime = symbolInfo.DeliverTime - }; + return exchangeInfo.ParseSymbol(key, symbolName); } + class ExchangeKeyedCache + { + private ExchangeInfo? _noKeyCache; + private ConcurrentDictionary _keyedCache = new ConcurrentDictionary(); + + public ExchangeInfo? Get(string? key) + { + if (key == null) + return _noKeyCache; + + if (_keyedCache.TryGetValue(key, out var exchangeInfo)) + return exchangeInfo; + + return null; + } + + public void Set(string? key, ExchangeInfo exchangeInfo) + { + if (key == null) + _noKeyCache = exchangeInfo; + else + _keyedCache[key] = exchangeInfo; + } + + public bool HasCached(string? key) + { + if (key == null) + { + if (_noKeyCache?.Symbols.Count > 0) + return true; + + foreach (var cache in _keyedCache.Values) + { + if (cache.Symbols.Count > 0) + return true; + } + + return false; + } + + return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Count > 0; + } + + public SharedSymbol? ParseSymbol(string? key, string symbolName) + { + SharedSymbol? symbolInfo = null; + if (key == null) + { + if (_noKeyCache != null) + { + if (!_noKeyCache.Symbols.TryGetValue(symbolName, out symbolInfo)) + return null; + + return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName) + { + DeliverTime = symbolInfo.DeliverTime + }; + } + + foreach(var cache in _keyedCache.Values) + { + if (cache.Symbols.TryGetValue(symbolName, out symbolInfo)) + { + return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName) + { + DeliverTime = symbolInfo.DeliverTime + }; + } + } + + return null; + } + + var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo); + if (!hasKeyedSet || exchangeInfo == null) + return null; + + if (exchangeInfo.Symbols.TryGetValue(symbolName, out symbolInfo)) + { + return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName) + { + DeliverTime = symbolInfo.DeliverTime + }; + } + + return null; + } + + public bool SupportsSymbol(string? key, string symbolName) + { + if (key == null) + { + if (_noKeyCache?.Symbols.ContainsKey(symbolName) == true) + return true; + + foreach(var cache in _keyedCache.Values) + { + if (cache.Symbols.ContainsKey(symbolName)) + return true; + } + + return false; + } + + return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.ContainsKey(symbolName); + } + + public bool SupportsSymbol(string? key, SharedSymbol symbol) + { + if (key == null) + { + if (_noKeyCache?.Symbols.Any(x => + x.Value.TradingMode == symbol.TradingMode + && x.Value.BaseAsset == symbol.BaseAsset + && x.Value.QuoteAsset == symbol.QuoteAsset) == true) + { + return true; + } + + foreach (var cache in _keyedCache.Values) + { + if (cache.Symbols.Any(x => + x.Value.TradingMode == symbol.TradingMode + && x.Value.BaseAsset == symbol.BaseAsset + && x.Value.QuoteAsset == symbol.QuoteAsset)) + { + return true; + } + } + + return false; + } + + return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Any(x => + x.Value.TradingMode == symbol.TradingMode + && x.Value.BaseAsset == symbol.BaseAsset + && x.Value.QuoteAsset == symbol.QuoteAsset); + } + + public SharedSymbol[] GetSymbolsForBaseAsset(string? key, string baseAsset) + { + if (key == null) + { + if (_noKeyCache != null) + { + return _noKeyCache.Symbols + .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) + .Select(x => x.Value) + .ToArray(); + } + + var result = new List(); + foreach(var cache in _keyedCache.Values) + { + result.AddRange(cache.Symbols + .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) + .Select(x => x.Value)); + } + + return result.ToArray(); + } + + var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo); + if (!hasKeyedSet || exchangeInfo == null) + return []; + + return exchangeInfo.Symbols + .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) + .Select(x => x.Value) + .ToArray(); + } + } + + class ExchangeInfo { public DateTime UpdateTime { get; set; } diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 452dce84..f1166de4 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.DependencyInjection; using System; @@ -24,7 +25,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddParameter(this Dictionary parameters, string key, string value) + public static void AddParameter(this IDictionary parameters, string key, string value) { parameters.Add(key, value); } @@ -35,7 +36,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddParameter(this Dictionary parameters, string key, object value) + public static void AddParameter(this IDictionary parameters, string key, object value) { parameters.Add(key, value); } @@ -46,7 +47,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddOptionalParameter(this Dictionary parameters, string key, object? value) + public static void AddOptionalParameter(this IDictionary parameters, string key, object? value) { if (value != null) parameters.Add(key, value); @@ -378,8 +379,6 @@ namespace CryptoExchange.Net services.AddTransient(x => (IDepositRestClient)client(x)!); if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (IKlineRestClient)client(x)!); - if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T))) - services.AddTransient(x => (IListenKeyRestClient)client(x)!); if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (IOrderBookRestClient)client(x)!); if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T))) diff --git a/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs index 205b231c..de5f88e9 100644 --- a/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs @@ -10,6 +10,10 @@ namespace CryptoExchange.Net.Interfaces.Clients /// public interface IBaseApiClient { + /// + /// Exchange name + /// + string Exchange { get; } /// /// Base address /// diff --git a/CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs index 0b8b91ba..c074ed24 100644 --- a/CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/IRestApiClient.cs @@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Interfaces.Clients /// bool Authenticated { get; } + /// + /// Configured credentials + /// + TApiCredentials? ApiCredentials { get; } + /// /// Set the API credentials for this API client /// diff --git a/CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs index a05494bf..ced0aac0 100644 --- a/CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs +++ b/CryptoExchange.Net/Interfaces/Clients/ISocketApiClient.cs @@ -84,6 +84,12 @@ namespace CryptoExchange.Net.Interfaces.Clients /// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid. /// bool Authenticated { get; } + + /// + /// Configured credentials + /// + TApiCredentials? ApiCredentials { get; } + /// /// Set the API credentials for this API client /// diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 0b94d6a1..18371c79 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Interfaces /// /// A cancellation token to stop the order book when canceled /// - Task> StartAsync(CancellationToken? ct = null); + Task StartAsync(CancellationToken? ct = null); /// /// Stop syncing the order book diff --git a/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs index a327f189..279f7d89 100644 --- a/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs @@ -18,32 +18,32 @@ namespace CryptoExchange.Net.Logging.Extensions _rateLimitRequestFailed = LoggerMessage.Define( LogLevel.Warning, new EventId(6000, "RateLimitRequestFailed"), - "[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}"); + "[Req {Id}] call to {Path} failed because of ratelimit guard {Guard}; {Limit}"); _rateLimitConnectionFailed = LoggerMessage.Define( LogLevel.Warning, new EventId(6001, "RateLimitConnectionFailed"), - "[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}"); + "[Sckt {Id}] connection failed because of ratelimit guard {Guard}; {Limit}"); _rateLimitDelayingRequest = LoggerMessage.Define( LogLevel.Warning, new EventId(6002, "RateLimitDelayingRequest"), - "[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}"); + "[Req {Id}] delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}"); _rateLimitDelayingConnection = LoggerMessage.Define( LogLevel.Warning, new EventId(6003, "RateLimitDelayingConnection"), - "[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}"); + "[Sckt {Id}] delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}"); _rateLimitAppliedConnection = LoggerMessage.Define( LogLevel.Trace, new EventId(6004, "RateLimitDelayingConnection"), - "[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); + "[Sckt {Id}] connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); _rateLimitAppliedRequest = LoggerMessage.Define( LogLevel.Trace, new EventId(6005, "RateLimitAppliedRequest"), - "[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); + "[Req {Id}] call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); } public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit) diff --git a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs index b8b7d49f..901d935c 100644 --- a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs @@ -28,67 +28,67 @@ namespace CryptoExchange.Net.Logging.Extensions _restApiErrorReceived = LoggerMessage.Define( LogLevel.Warning, new EventId(4000, "RestApiErrorReceived"), - "[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}"); + "[Req {RequestId}] {ResponseStatusCode} - error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}"); _restApiResponseReceived = LoggerMessage.Define( LogLevel.Debug, new EventId(4001, "RestApiResponseReceived"), - "[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}"); + "[Req {RequestId}] {ResponseStatusCode} - response received in {ResponseTime}ms: {OriginalData}"); _restApiFailedToSyncTime = LoggerMessage.Define( LogLevel.Debug, new EventId(4002, "RestApiFailedToSyncTime"), - "[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}"); + "[Req {RequestId}] failed to sync time, aborting request: {ErrorMessage}"); _restApiNoApiCredentials = LoggerMessage.Define( LogLevel.Warning, new EventId(4003, "RestApiNoApiCredentials"), - "[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided"); + "[Req {RequestId}] request {RestApiUri} failed because no ApiCredentials were provided"); _restApiCreatingRequest = LoggerMessage.Define( LogLevel.Information, new EventId(4004, "RestApiCreatingRequest"), - "[Req {RequestId}] Creating request for {RestApiUri}"); + "[Req {RequestId}] creating request for {RestApiUri}"); _restApiSendingRequest = LoggerMessage.Define( LogLevel.Trace, new EventId(4005, "RestApiSendingRequest"), - "[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}"); + "[Req {RequestId}] sending {Method} {Signed} request to {RestApiUri}{Query}"); _restApiRateLimitRetry = LoggerMessage.Define( LogLevel.Warning, new EventId(4006, "RestApiRateLimitRetry"), - "[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}"); + "[Req {RequestId}] received ratelimit error, retrying after {Timestamp}"); _restApiRateLimitPauseUntil = LoggerMessage.Define( LogLevel.Warning, new EventId(4007, "RestApiRateLimitPauseUntil"), - "[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}"); + "[Req {RequestId}] ratelimit error from server, pausing requests until {Until}"); _restApiSendRequest = LoggerMessage.Define( LogLevel.Debug, new EventId(4008, "RestApiSendRequest"), - "[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}"); + "[Req {RequestId}] sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}"); _restApiCheckingCache = LoggerMessage.Define( LogLevel.Trace, new EventId(4009, "RestApiCheckingCache"), - "Checking cache for key {Key}"); + "checking cache for key {Key}"); _restApiCacheHit = LoggerMessage.Define( LogLevel.Trace, new EventId(4010, "RestApiCacheHit"), - "Cache hit for key {Key}"); + "cache hit for key {Key}"); _restApiCacheNotHit = LoggerMessage.Define( LogLevel.Trace, new EventId(4011, "RestApiCacheNotHit"), - "Cache not hit for key {Key}"); + "cache not hit for key {Key}"); _restApiCancellationRequested = LoggerMessage.Define( LogLevel.Debug, new EventId(4012, "RestApiCancellationRequested"), - "[Req {RequestId}] Request cancelled by user"); + "[Req {RequestId}] request cancelled by user"); } diff --git a/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs index 75dfe262..69e0fafc 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs @@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Logging.Extensions _attemptingToAuthenticate = LoggerMessage.Define( LogLevel.Debug, new EventId(3006, "AttemptingToAuthenticate"), - "[Sckt {SocketId}] Attempting to authenticate"); + "[Sckt {SocketId}] attempting to authenticate"); _authenticationFailed = LoggerMessage.Define( LogLevel.Warning, @@ -76,12 +76,12 @@ namespace CryptoExchange.Net.Logging.Extensions _failedToDetermineConnectionUrl = LoggerMessage.Define( LogLevel.Warning, new EventId(3009, "FailedToDetermineConnectionUrl"), - "Failed to determine connection url: {ErrorMessage}"); + "failed to determine connection url: {ErrorMessage}"); _connectionAddressSetTo = LoggerMessage.Define( LogLevel.Debug, new EventId(3010, "ConnectionAddressSetTo"), - "Connection address set to {ConnectionAddress}"); + "connection address set to {ConnectionAddress}"); _socketCreatedForAddress = LoggerMessage.Define( LogLevel.Debug, @@ -91,37 +91,37 @@ namespace CryptoExchange.Net.Logging.Extensions _unsubscribingAll = LoggerMessage.Define( LogLevel.Information, new EventId(3013, "UnsubscribingAll"), - "Unsubscribing all {SubscriptionCount} subscriptions"); + "unsubscribing all {SubscriptionCount} subscriptions"); _disposingSocketClient = LoggerMessage.Define( LogLevel.Debug, new EventId(3015, "DisposingSocketClient"), - "Disposing socket client, closing all subscriptions"); + "disposing socket client, closing all subscriptions"); _unsubscribingSubscription = LoggerMessage.Define( LogLevel.Information, new EventId(3016, "UnsubscribingSubscription"), - "[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}"); + "[Sckt {SocketId}] unsubscribing subscription {SubscriptionId}"); _reconnectingAllConnections = LoggerMessage.Define( LogLevel.Information, new EventId(3017, "ReconnectingAll"), - "Reconnecting all {ConnectionCount} connections"); + "reconnecting all {ConnectionCount} connections"); _addingRetryAfterGuard = LoggerMessage.Define( LogLevel.Warning, new EventId(3018, "AddRetryAfterGuard"), - "Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited"); + "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"); + "timeout while waiting for existing socket reconnection, failing request"); _waitedForReconnectingSocket = LoggerMessage.Define( LogLevel.Trace, new EventId(3020, "WaitedForReconnectingSocket"), - "Waited for reconnecting socket for {Timespan}ms"); + "waited for reconnecting socket for {Timespan}ms"); } public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId) diff --git a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs index 290db82d..c85d3ab0 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs @@ -60,7 +60,7 @@ namespace CryptoExchange.Net.Logging.Extensions _unknownExceptionWhileProcessingReconnection = LoggerMessage.Define( LogLevel.Warning, new EventId(2003, "UnknownExceptionWhileProcessingReconnection"), - "[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again"); + "[Sckt {SocketId}] unknown exception while processing reconnection, reconnecting again"); _webSocketErrorCodeAndDetails = LoggerMessage.Define( LogLevel.Warning, diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs deleted file mode 100644 index 6837fcb2..00000000 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ /dev/null @@ -1,592 +0,0 @@ -using CryptoExchange.Net.SharedApis; -using System; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -namespace CryptoExchange.Net.Objects -{ - /// - /// The result of an operation - /// - public class CallResult - { - /// - /// Static success result - /// - public static CallResult SuccessResult { get; } = new CallResult(null); - - /// - /// An error if the call didn't succeed, will always be filled if Success = false - /// - public Error? Error { get; internal set; } - - /// - /// Whether the call was successful - /// - public bool Success => Error == null; - - /// - /// ctor - /// - /// - public CallResult(Error? error) - { - Error = error; - } - - /// - /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) - /// - /// - public static implicit operator bool(CallResult obj) - { - return obj?.Success == true; - } - - /// - public override string ToString() - { - return Success ? $"Success" : $"Error: {Error}"; - } - } - - /// - /// The result of an operation - /// - /// - public class CallResult: CallResult - { - /// - /// The data returned by the call, only available when Success = true - /// - public T Data { get; internal set; } - - /// - /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options - /// - public string? OriginalData { get; internal set; } - - /// - /// ctor - /// - /// - /// - /// -#pragma warning disable 8618 - public CallResult([AllowNull]T data, string? originalData, Error? error): base(error) -#pragma warning restore 8618 - { - OriginalData = originalData; -#pragma warning disable 8601 - Data = data; -#pragma warning restore 8601 - } - - /// - /// Create a new data result - /// - /// The data to return - public CallResult(T data) : this(data, null, null) { } - - /// - /// Create a new error result - /// - /// The error to return - public CallResult(Error error) : this(default, null, error) { } - - /// - /// Create a new error result - /// - /// The error to return - /// The original response data - public CallResult(Error error, string? originalData) : this(default, originalData, error) { } - - /// - /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) - /// - /// - public static implicit operator bool(CallResult obj) - { - return obj?.Success == true; - } - - /// - /// Whether the call was successful or not. Useful for nullability checking. - /// - /// The data returned by the call. - /// on failure. - /// true when succeeded, false otherwise. - public bool GetResultOrError([MaybeNullWhen(false)] out T data, [NotNullWhen(false)] out Error? error) - { - if (Success) - { - data = Data!; - error = null; - - return true; - } - else - { - data = default; - error = Error!; - - return false; - } - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public CallResult As([AllowNull] K data) - { - return new CallResult(data, OriginalData, Error); - } - - /// - /// Copy as a dataless result - /// - /// - public CallResult AsDataless() - { - if (Error != null ) - return new CallResult(Error); - - return SuccessResult; - } - - /// - /// Copy as a dataless result - /// - /// - public CallResult AsDatalessError(Error error) - { - return new CallResult(error); - } - - /// - /// Copy the CallResult to a new data type - /// - /// The new type - /// The data - /// The error returned - /// - public CallResult AsErrorWithData(Error error, K data) - { - return new CallResult(data, OriginalData, error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The error to return - /// - public CallResult AsError(Error error) - { - return new CallResult(default, OriginalData, error); - } - - /// - public override string ToString() - { - return Success ? $"Success" : $"Error: {Error}"; - } - } - - /// - /// The result of a request - /// - public class WebCallResult : CallResult - { - /// - /// The request http method - /// - public HttpMethod? RequestMethod { get; set; } - - /// - /// HTTP protocol version - /// - public Version? HttpVersion { get; set; } - - /// - /// The headers sent with the request - /// - public HttpRequestHeaders? RequestHeaders { get; set; } - - /// - /// The request id - /// - public int? RequestId { get; set; } - - /// - /// The url which was requested - /// - public string? RequestUrl { get; set; } - - /// - /// The body of the request - /// - public string? RequestBody { get; set; } - - /// - /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options - /// - public string? OriginalData { get; internal set; } - - /// - /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. - /// - public HttpStatusCode? ResponseStatusCode { get; set; } - - /// - /// The response headers - /// - public HttpResponseHeaders? ResponseHeaders { get; set; } - - /// - /// The time between sending the request and receiving the response - /// - public TimeSpan? ResponseTime { get; set; } - - /// - /// ctor - /// - public WebCallResult( - HttpStatusCode? code, - Version? httpVersion, - HttpResponseHeaders? responseHeaders, - TimeSpan? responseTime, - string? originalData, - int? requestId, - string? requestUrl, - string? requestBody, - HttpMethod? requestMethod, - HttpRequestHeaders? requestHeaders, - Error? error) : base(error) - { - ResponseStatusCode = code; - HttpVersion = httpVersion; - ResponseHeaders = responseHeaders; - ResponseTime = responseTime; - RequestId = requestId; - OriginalData = originalData; - - RequestUrl = requestUrl; - RequestBody = requestBody; - RequestHeaders = requestHeaders; - RequestMethod = requestMethod; - } - - /// - /// ctor - /// - /// - public WebCallResult(Error error): base(error) { } - - /// - /// Return the result as an error result - /// - /// The error returned - /// - public WebCallResult AsError(Error error) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public WebCallResult As([AllowNull] K data) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade mode the result applies to - /// The data - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] K data) - { - return new ExchangeWebResult(exchange, tradeMode, this.As(data)); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade modes the result applies to - /// The data - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] K data) - { - return new ExchangeWebResult(exchange, tradeModes, this.As(data)); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The error returned - /// - public WebCallResult AsError(Error error) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error); - } - - /// - public override string ToString() - { - return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}"; - } - } - - /// - /// The result of a request - /// - /// - public class WebCallResult: CallResult - { - /// - /// The request http method - /// - public HttpMethod? RequestMethod { get; set; } - - /// - /// HTTP protocol version - /// - public Version? HttpVersion { get; set; } - - /// - /// The headers sent with the request - /// - public HttpRequestHeaders? RequestHeaders { get; set; } - - /// - /// The request id - /// - public int? RequestId { get; set; } - - /// - /// The url which was requested - /// - public string? RequestUrl { get; set; } - - /// - /// The body of the request - /// - public string? RequestBody { get; set; } - - /// - /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. - /// - public HttpStatusCode? ResponseStatusCode { get; set; } - - /// - /// Length in bytes of the response - /// - public long? ResponseLength { get; set; } - - /// - /// The response headers - /// - public HttpResponseHeaders? ResponseHeaders { get; set; } - - /// - /// The time between sending the request and receiving the response - /// - public TimeSpan? ResponseTime { get; set; } - - /// - /// The data source of this result - /// - public ResultDataSource DataSource { get; set; } = ResultDataSource.Server; - - /// - /// Create a new result - /// - public WebCallResult( - HttpStatusCode? code, - Version? httpVersion, - HttpResponseHeaders? responseHeaders, - TimeSpan? responseTime, - long? responseLength, - string? originalData, - int? requestId, - string? requestUrl, - string? requestBody, - HttpMethod? requestMethod, - HttpRequestHeaders? requestHeaders, - ResultDataSource dataSource, - [AllowNull] T data, - Error? error) : base(data, originalData, error) - { - HttpVersion = httpVersion; - ResponseStatusCode = code; - ResponseHeaders = responseHeaders; - ResponseTime = responseTime; - ResponseLength = responseLength; - - RequestId = requestId; - RequestUrl = requestUrl; - RequestBody = requestBody; - RequestHeaders = requestHeaders; - RequestMethod = requestMethod; - DataSource = dataSource; - } - - /// - /// Copy as a dataless result - /// - /// - public new WebCallResult AsDataless() - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); - } - /// - /// Copy as a dataless result - /// - /// - public new WebCallResult AsDatalessError(Error error) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); - } - - /// - /// Create a new error result - /// - /// The error - public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public new WebCallResult As([AllowNull] K data) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The error returned - /// - public new WebCallResult AsError(Error error) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data - /// The error returned - /// - public new WebCallResult AsErrorWithData(Error error, K data) - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The exchange - /// Trade mode the result applies to - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode) - { - return new ExchangeWebResult(exchange, tradeMode, this); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The exchange - /// Trade modes the result applies to - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[] tradeModes) - { - return new ExchangeWebResult(exchange, tradeModes, this); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade mode the result applies to - /// Data - /// Next page request - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null) - { - return new ExchangeWebResult(exchange, tradeMode, As(data), nextPageRequest); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade modes the result applies to - /// Data - /// Next page token - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null) - { - return new ExchangeWebResult(exchange, tradeModes, As(data), nextPageRequest); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult with a specific error - /// - /// The new type - /// The exchange - /// The error returned - /// - public ExchangeWebResult AsExchangeError(string exchange, Error error) - { - return new ExchangeWebResult(exchange, null, AsError(error)); - } - - /// - /// Return a copy of this result with data source set to cache - /// - /// - internal WebCallResult Cached() - { - return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error); - } - - /// - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append(Success ? $"Success response" : $"Error response: {Error}"); - if (ResponseLength != null) - sb.Append($", {ResponseLength} bytes"); - if (ResponseTime != null) - sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms"); - - return sb.ToString(); - } - } -} diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 4084b962..d894f325 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -149,6 +149,14 @@ namespace CryptoExchange.Net.Objects /// public class ServerError : Error { + /// + /// ctor + /// + public ServerError(ErrorType type, string message, Exception? exception = null) + : base(null, new ErrorInfo(type, message), exception) + { + } + /// /// ctor /// diff --git a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs index c1907f1a..5c062bdb 100644 --- a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs @@ -44,7 +44,7 @@ namespace CryptoExchange.Net.Objects.Options /// public override string ToString() { - return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}"; + return $"Proxy: {(Proxy == null ? "-" : "set")}"; } } } diff --git a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs index e7869107..dfbb87c1 100644 --- a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs @@ -105,6 +105,12 @@ namespace CryptoExchange.Net.Objects.Options target.Environment = Environment; return target; } + + /// + public override string ToString() + { + return $"{base.ToString()} | Environment: {Environment.Name}"; + } } /// @@ -131,7 +137,7 @@ namespace CryptoExchange.Net.Objects.Options /// public override string ToString() { - return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; + return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; } } diff --git a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs index 3e5f3cf7..c01bad9c 100644 --- a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs @@ -132,6 +132,12 @@ namespace CryptoExchange.Net.Objects.Options target.Environment = Environment; return target; } + + /// + public override string ToString() + { + return $"{base.ToString()} | Environment: {Environment.Name}"; + } } /// @@ -159,7 +165,7 @@ namespace CryptoExchange.Net.Objects.Options /// public override string ToString() { - return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; + return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; } } } diff --git a/CryptoExchange.Net/Objects/ParameterCollection.cs b/CryptoExchange.Net/Objects/ParameterCollection.cs deleted file mode 100644 index 723fb6f7..00000000 --- a/CryptoExchange.Net/Objects/ParameterCollection.cs +++ /dev/null @@ -1,343 +0,0 @@ -using CryptoExchange.Net.Attributes; -using CryptoExchange.Net.Converters.SystemTextJson; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; - -namespace CryptoExchange.Net.Objects -{ - /// - /// Parameters collection - /// - public class ParameterCollection : Dictionary - { - /// - public new void Add(string key, object value) - { - if (value == null) - throw new ArgumentNullException(key); - - base.Add(key, value); - } - - /// - /// Add an optional parameter. Not added if value is null - /// - /// - /// - public void AddOptional(string key, object? value) - { - if (value != null) - base.Add(key, value); - } - - /// - /// Add a decimal value as string - /// - /// - /// - public void AddString(string key, decimal value) - { - base.Add(key, value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a decimal value as string. Not added if value is null - /// - /// - /// - public void AddOptionalString(string key, decimal? value) - { - if (value != null) - base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a int value as string - /// - /// - /// - public void AddString(string key, int value) - { - base.Add(key, value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a int value as string. Not added if value is null - /// - /// - /// - public void AddOptionalString(string key, int? value) - { - if (value != null) - base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a long value as string - /// - /// - /// - public void AddString(string key, long value) - { - base.Add(key, value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a long value as string. Not added if value is null - /// - /// - /// - public void AddOptionalString(string key, long? value) - { - if (value != null) - base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a DateTime value as string - /// - public void AddString(string key, DateTime value) - { - base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ")); - } - - /// - /// Add a DateTime value as string. Not added if value is null - /// - public void AddOptionalString(string key, DateTime? value) - { - if (value != null) - base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ")); - } - - /// - /// Add a datetime value as milliseconds timestamp - /// - /// - /// - public void AddMilliseconds(string key, DateTime value) - { - base.Add(key, DateTimeConverter.ConvertToMilliseconds(value)); - } - - /// - /// Add a datetime value as milliseconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalMilliseconds(string key, DateTime? value) - { - if (value != null) - base.Add(key, DateTimeConverter.ConvertToMilliseconds(value)); - } - - /// - /// Add a datetime value as milliseconds timestamp - /// - /// - /// - public void AddMillisecondsString(string key, DateTime value) - { - base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a datetime value as milliseconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalMillisecondsString(string key, DateTime? value) - { - if (value != null) - base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Add a datetime value as seconds timestamp - /// - /// - /// - public void AddSeconds(string key, DateTime value) - { - base.Add(key, DateTimeConverter.ConvertToSeconds(value)); - } - - /// - /// Add a datetime value as seconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalSeconds(string key, DateTime? value) - { - if (value != null) - base.Add(key, DateTimeConverter.ConvertToSeconds(value)); - } - - /// - /// Add a datetime value as string seconds timestamp - /// - /// - /// - public void AddSecondsString(string key, DateTime value) - { - base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); - } - - /// - /// Add a datetime value as string seconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalSecondsString(string key, DateTime? value) - { - if (value != null) - base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); - } - - /// - /// Add an enum value as the string value as mapped using the - /// -#if NET5_0_OR_GREATER - public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) -#else - public void AddEnum(string key, T value) -#endif - where T : struct, Enum - { - base.Add(key, EnumConverter.GetString(value)!); - } - - /// - /// Add an enum value as the string value as mapped using the - /// - /// - /// -#if NET5_0_OR_GREATER - public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) -#else - public void AddEnumAsInt(string key, T value) -#endif - where T : struct, Enum - { - var stringVal = EnumConverter.GetString(value)!; - base.Add(key, int.Parse(stringVal)!); - } - - /// - /// Add an enum value as the string value as mapped using the . Not added if value is null - /// - /// - /// -#if NET5_0_OR_GREATER - public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) -#else - public void AddOptionalEnum(string key, T? value) -#endif - where T : struct, Enum - { - if (value != null) - base.Add(key, EnumConverter.GetString(value)); - } - - /// - /// Add an enum value as the string value as mapped using the . Not added if value is null - /// -#if NET5_0_OR_GREATER - public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) -#else - public void AddOptionalEnumAsInt(string key, T? value) -#endif - where T : struct, Enum - { - if (value != null) - { - var stringVal = EnumConverter.GetString(value); - base.Add(key, int.Parse(stringVal)); - } - } - - /// - /// Add key as comma separated values - /// - public void AddCommaSeparated(string key, IEnumerable values) - { - base.Add(key, string.Join(",", values)); - } - - /// - /// Add key as comma separated values if there are values provided - /// - public void AddOptionalCommaSeparated(string key, IEnumerable? values) - { - if (values == null || !values.Any()) - return; - - base.Add(key, string.Join(",", values)); - } - - /// - /// Add key as comma separated values - /// -#if NET5_0_OR_GREATER - public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable values) -#else - public void AddCommaSeparated(string key, IEnumerable values) -#endif - where T : struct, Enum - { - base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x)))); - } - - /// - /// Add key as comma separated values if there are values provided - /// -#if NET5_0_OR_GREATER - public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable? values) -#else - public void AddOptionalCommaSeparated(string key, IEnumerable? values) -#endif - where T : struct, Enum - { - if (values == null || !values.Any()) - return; - - base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x)))); - } - - /// - /// Add key as boolean lower case value - /// - public void AddBoolString(string key, bool value) - { - base.Add(key, value.ToString().ToLower()); - } - - /// - /// Add key as boolean lower case value if it's not null - /// - public void AddOptionalBoolString(string key, bool? value) - { - if (value == null) - return; - - base.Add(key, value.ToString()!.ToLower()); - } - - - /// - /// Set the request body. Can be used to specify a simple value or array as the body instead of an object - /// - /// Body to set - /// - public void SetBody(object body) - { - if (this.Any()) - throw new InvalidOperationException("Can't set body when other parameters already specified"); - - base.Add(Constants.BodyPlaceHolderKey, body); - } - } -} diff --git a/CryptoExchange.Net/Objects/ParameterSerializationSettings.cs b/CryptoExchange.Net/Objects/ParameterSerializationSettings.cs new file mode 100644 index 00000000..966e0917 --- /dev/null +++ b/CryptoExchange.Net/Objects/ParameterSerializationSettings.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects +{ + /// + /// Settings for parameter serialization + /// + public class ParameterSerializationSettings + { + /// + /// Default serialization settings + /// + public static ParameterSerializationSettings Default { get; } = new ParameterSerializationSettings(); + + /// + /// Whether to sort the parameters + /// + public bool Sort { get; set; } = true; + /// + /// The parameter comparer when sorting + /// + public IComparer? SortComparer { get; set; } + /// + /// Decimal serialization type + /// + public DecimalSerialization Decimal { get; set; } = DecimalSerialization.Number; + /// + /// DateTime serialization type + /// + public DateTimeSerialization DateTimes { get; set; } = DateTimeSerialization.MillisecondsNumber; + /// + /// Boolean serialization type + /// + public BoolSerialization Bool { get; set; } = BoolSerialization.Bool; + /// + /// Integer serialization type + /// + public IntegerSerialization Integer { get; set; } = IntegerSerialization.Number; + /// + /// Enum serialization type + /// + public EnumSerialization Enum { get; set; } = EnumSerialization.String; + /// + /// Array serialization type + /// + public ArrayParametersSerialization Array { get; set; } = ArrayParametersSerialization.Array; + } + + + /// + /// Type of decimal value serialization + /// + public enum DecimalSerialization + { + /// + /// Decimals should be serialized as numbers + /// + Number, + /// + /// Decimals should be strings + /// + String + } + + /// + /// Type of DateTime value serialization + /// + public enum DateTimeSerialization + { + /// + /// DateTimes should be serialized as milliseconds number + /// + MillisecondsNumber, + /// + /// DateTimes should be serialized as milliseconds string + /// + MillisecondsString, + /// + /// DateTimes should be serialized as seconds number + /// + SecondsNumber, + /// + /// DateTimes should be serialized as seconds string + /// + SecondsString, + /// + /// DateTimes should be serialized as microseconds number + /// + MicrosecondsNumber, + /// + /// DateTimes should be serialized as microseconds string + /// + MicrosecondsString, + /// + /// DateTimes should be serialized as ISO 8601 string + /// + Rfc3339String + } + + /// + /// Type of boolean value serialization + /// + public enum BoolSerialization + { + /// + /// Booleans should be serialized as bool values + /// + Bool, + /// + /// Booleans should be serialized as strings + /// + String + } + + /// + /// Type of integer value serialization + /// + public enum IntegerSerialization + { + /// + /// Integers should be serialized as integer values + /// + Number, + /// + /// Integers should be serialized as strings + /// + String + } + + /// + /// Type of enum value serialization + /// + public enum EnumSerialization + { + /// + /// Enums should be serialized as integer values + /// + Number, + /// + /// Enums should be serialized as strings + /// + String + } +} diff --git a/CryptoExchange.Net/Objects/Parameters.cs b/CryptoExchange.Net/Objects/Parameters.cs new file mode 100644 index 00000000..38d2723b --- /dev/null +++ b/CryptoExchange.Net/Objects/Parameters.cs @@ -0,0 +1,375 @@ +using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Converters.SystemTextJson; +using CryptoExchange.Net.Interfaces; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; + +namespace CryptoExchange.Net.Objects +{ + /// + /// Set of parameters + /// + public class Parameters : IDictionary + { + private readonly ParameterSerializationSettings _serializationSettings; + private IDictionary _parameters; + private object? _value; + + /// + public object? BodyValue => _value; + + /// + public ICollection Keys => _parameters.Keys; + + /// + public ICollection Values => _parameters.Values; + + /// + public int Count => _parameters.Count; + + /// + public bool IsReadOnly => _parameters.IsReadOnly; + + /// + /// Whether any parameters are defined + /// + public bool Empty => _parameters.Count == 0 && _value == null; + + /// + public object this[string key] { get => _parameters[key]; set => _parameters[key] = value; } + + /// + /// ctor + /// + /// Serialization settings + public Parameters(ParameterSerializationSettings serializationSettings) + { + _serializationSettings = serializationSettings; + if (_serializationSettings.Sort) + _parameters = new SortedDictionary(_serializationSettings.SortComparer); + else + _parameters = new Dictionary(); + + } + + /// + /// ctor + /// + /// Serialization settings + /// Body value + public Parameters(object value, ParameterSerializationSettings serializationSettings) + { + _parameters = new Dictionary(); + _serializationSettings = serializationSettings; + _value = value; + } + + /// + /// Add a short value if it is not null + /// + public void Add(string key, short? value, IntegerSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a short value + /// + public void Add(string key, short value, IntegerSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.Integer; + if (serializationToUse == IntegerSerialization.String) + _parameters.Add(key, value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == IntegerSerialization.Number) + _parameters.Add(key, value); + else + throw new ArgumentException("Unknown Integer serialization setting"); + } + + /// + /// Add an int value if it is not null + /// + public void Add(string key, int? value, IntegerSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add an int value + /// + public void Add(string key, int value, IntegerSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.Integer; + if (serializationToUse == IntegerSerialization.String) + _parameters.Add(key, value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == IntegerSerialization.Number) + _parameters.Add(key, value); + else + throw new ArgumentException("Unknown Integer serialization setting"); + } + + /// + /// Add a long value if it is not null + /// + public void Add(string key, long? value, IntegerSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a long value + /// + public void Add(string key, long value, IntegerSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.Integer; + if (serializationToUse == IntegerSerialization.String) + _parameters.Add(key, value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == IntegerSerialization.Number) + _parameters.Add(key, value); + else + throw new ArgumentException("Unknown Integer serialization setting"); + } + + /// + /// Add a decimal value if it is not null + /// + public void Add(string key, decimal? value, DecimalSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a decimal value + /// + public void Add(string key, decimal value, DecimalSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.Decimal; + if (serializationToUse == DecimalSerialization.String) + _parameters.Add(key, value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == DecimalSerialization.Number) + _parameters.Add(key, value); + else + throw new ArgumentException("Unknown Decimal serialization setting"); + } + + /// + /// Add a double value if it is not null + /// + public void Add(string key, double? value, DecimalSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a double value + /// + public void Add(string key, double value, DecimalSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.Decimal; + if (serializationToUse == DecimalSerialization.String) + _parameters.Add(key, value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == DecimalSerialization.Number) + _parameters.Add(key, value); + else + throw new ArgumentException("Unknown Decimal serialization setting"); + } + + /// + /// Add a bool value if it is not null + /// + public void Add(string key, bool? value, BoolSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a bool value + /// + public void Add(string key, bool value, BoolSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.Bool; + if (serializationToUse == BoolSerialization.String) + _parameters.Add(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + else if (serializationToUse == BoolSerialization.Bool) + _parameters.Add(key, value); + else + throw new ArgumentException("Unknown Bool serialization setting"); + } + + /// + /// Add key as comma separated values if there are values provided + /// + public void AddCommaSeparated(string key, IEnumerable? values) + { + if (values == null || !values.Any()) + return; + + _parameters.Add(key, string.Join(",", values)); + } + + /// + /// Add key as comma separated values + /// +#if NET5_0_OR_GREATER + public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable values) +#else + public void AddCommaSeparated(string key, IEnumerable? values) +#endif + where T : struct, Enum + { + if (values == null || !values.Any()) + return; + + _parameters.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x)))); + } + + /// + /// Add an enum value if it is not null + /// + public void Add< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] +# endif + T>(string key, T? value, EnumSerialization? serialization = null) + where T : struct, Enum + + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a enum value + /// + public void Add< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] +#endif + T>(string key, T value, EnumSerialization? serialization = null) + where T : struct, Enum + { + var serializationToUse = serialization ?? _serializationSettings.Enum; + if (serializationToUse == EnumSerialization.String) + _parameters.Add(key, EnumConverter.GetString(value)); + else if (serializationToUse == EnumSerialization.Number) + _parameters.Add(key, int.Parse(EnumConverter.GetString(value), CultureInfo.InvariantCulture)); + else + throw new ArgumentException("Unknown Integer serialization setting"); + } + + /// + /// Add a DateTime value if it is not null + /// + public void Add(string key, DateTime? value, DateTimeSerialization? serialization = null) + { + if (value == null) + return; + + Add(key, value.Value, serialization); + } + + /// + /// Add a DateTime value + /// + public void Add(string key, DateTime value, DateTimeSerialization? serialization = null) + { + var serializationToUse = serialization ?? _serializationSettings.DateTimes; + if (serializationToUse == DateTimeSerialization.MillisecondsNumber) + _parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value)); + else if (serializationToUse == DateTimeSerialization.MillisecondsString) + _parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == DateTimeSerialization.SecondsNumber) + _parameters.Add(key, DateTimeConverter.ConvertToSeconds(value)); + else if (serializationToUse == DateTimeSerialization.SecondsString) + _parameters.Add(key, DateTimeConverter.ConvertToSeconds(value).Value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == DateTimeSerialization.MicrosecondsNumber) + _parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value)); + else if (serializationToUse == DateTimeSerialization.MicrosecondsString) + _parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value).Value.ToString(CultureInfo.InvariantCulture)); + else if (serializationToUse == DateTimeSerialization.Rfc3339String) + _parameters.Add(key, value.ToRfc3339String()); + else + throw new ArgumentException("Unknown DateTime serialization setting"); + } + + /// + /// Add a string value if it is not null + /// + public void Add(string key, string? value) + { + if (value == null) + return; + + _parameters.Add(key, value); + } + + /// + /// Add an array of values if there are values provided + /// + public void AddArray(string key, IEnumerable? values) + { + if (values == null || !values.Any()) + return; + + _parameters.Add(key, values is T[] arr ? arr : values.ToArray()); + } + + /// + /// Add a raw object value if it is not null + /// + public void AddRaw(string key, object? value) + { + if (value == null) + return; + + _parameters.Add(key, value); + } + + /// + public void Add(string key, object value) => _parameters.Add(key, value); + /// + public bool ContainsKey(string key) => _parameters.ContainsKey(key); + /// + public bool Remove(string key) => _parameters.Remove(key); + /// + public bool TryGetValue(string key, out object value) => _parameters.TryGetValue(key, out value!); + /// + public void Add(KeyValuePair item) => _parameters.Add(item.Key, item.Value); + /// + public void Clear() => _parameters.Clear(); + /// + public bool Contains(KeyValuePair item) => _parameters.ContainsKey(item.Key) && _parameters[item.Key] == item.Value; + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _parameters.CopyTo(array, arrayIndex); + /// + public bool Remove(KeyValuePair item) => _parameters.Remove(item.Key); + /// + public IEnumerator> GetEnumerator() => _parameters.GetEnumerator(); + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/CryptoExchange.Net/Objects/PlatformInfo.cs b/CryptoExchange.Net/Objects/PlatformInfo.cs index 0a5de416..37060999 100644 --- a/CryptoExchange.Net/Objects/PlatformInfo.cs +++ b/CryptoExchange.Net/Objects/PlatformInfo.cs @@ -33,11 +33,23 @@ /// Centralization type /// public CentralizationType CentralizationType { get; } + /// + /// Supported environments + /// + public string[] SupportedEnvironments { get; } /// /// ctor /// - public PlatformInfo(string id, string displayName, string logo, string url, string[] apiDocsUrl, PlatformType platformType, CentralizationType centralizationType) + public PlatformInfo( + string id, + string displayName, + string logo, + string url, + string[] apiDocsUrl, + PlatformType platformType, + CentralizationType centralizationType, + string[] supportedEnvironments) { Id = id; DisplayName = displayName; @@ -46,6 +58,7 @@ ApiDocsUrl = apiDocsUrl; PlatformType = platformType; CentralizationType = centralizationType; + SupportedEnvironments = supportedEnvironments; } } } diff --git a/CryptoExchange.Net/Objects/RequestDefinition.cs b/CryptoExchange.Net/Objects/RequestDefinition.cs index 0aaec7e1..3a782274 100644 --- a/CryptoExchange.Net/Objects/RequestDefinition.cs +++ b/CryptoExchange.Net/Objects/RequestDefinition.cs @@ -9,9 +9,14 @@ namespace CryptoExchange.Net.Objects public class RequestDefinition { private string? _stringRep; + private string? _fullUrl; // Basics + /// + /// Base address of the request + /// + public string BaseAddress { get; set; } /// /// Path of the request /// @@ -77,13 +82,31 @@ namespace CryptoExchange.Net.Objects /// public bool? ForcePathEndWithSlash { get; set; } + /// + /// Full url, host + path + /// + public string FullUrl + { + get + { + if (_fullUrl != null) + return _fullUrl; + + var result = BaseAddress.AppendPath(Path); + if (ForcePathEndWithSlash == true && !result.EndsWith("/")) + result += "/"; + + _fullUrl = result; + return _fullUrl; + } + } + /// /// ctor /// - /// - /// - public RequestDefinition(string path, HttpMethod method) + public RequestDefinition(string baseAddress, string path, HttpMethod method) { + BaseAddress = baseAddress; Path = path; Method = method; diff --git a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs index 28354772..0a6477ad 100644 --- a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs +++ b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.RateLimiting.Interfaces; using System.Collections.Concurrent; +using System.IO; using System.Net.Http; namespace CryptoExchange.Net.Objects @@ -15,27 +16,30 @@ namespace CryptoExchange.Net.Objects /// Get a definition if it is already in the cache or create a new definition and add it to the cache /// /// The HttpMethod + /// The base address/host /// Endpoint path /// Endpoint is authenticated /// - public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false) - => GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null); + public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, bool authenticated = false) + => GetOrCreate(method, baseAddress, path, null, 0, authenticated, null, null, null, null, null, null, null); /// /// Get a definition if it is already in the cache or create a new definition and add it to the cache /// /// The HttpMethod + /// The base address/host /// Endpoint path /// The rate limit gate /// Request weight /// Endpoint is authenticated /// - public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false) - => GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null); + public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false) + => GetOrCreate(method, baseAddress, path, rateLimitGate, weight, authenticated, null, null, null, null, null, null, null); /// /// Get a definition if it is already in the cache or create a new definition and add it to the cache /// + /// The base address/host /// The HttpMethod /// Endpoint path /// The rate limit gate @@ -48,9 +52,11 @@ namespace CryptoExchange.Net.Objects /// Prevent request caching /// Try parse the response even when status is not success /// Force trailing `/` + /// Optional request identifier override /// public RequestDefinition GetOrCreate( HttpMethod method, + string baseAddress, string path, IRateLimitGate? rateLimitGate, int weight, @@ -61,45 +67,13 @@ namespace CryptoExchange.Net.Objects ArrayParametersSerialization? arraySerialization = null, bool? preventCaching = null, bool? tryParseOnNonSuccess = null, - bool? forcePathEndWithSlash = null) - => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// Request identifier - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// The rate limit guard for this specific endpoint - /// Request weight - /// Endpoint is authenticated - /// Request body format - /// Parameter position - /// Array serialization type - /// Prevent request caching - /// Try parse the response even when status is not success - /// Force trailing `/` - /// - public RequestDefinition GetOrCreate( - string identifier, - HttpMethod method, - string path, - IRateLimitGate? rateLimitGate, - int weight, - bool authenticated, - IRateLimitGuard? limitGuard = null, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - bool? preventCaching = null, - bool? tryParseOnNonSuccess = null, - bool? forcePathEndWithSlash = null) + bool? forcePathEndWithSlash = null, + string? identifier = null) { - - if (!_definitions.TryGetValue(identifier, out var def)) + var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}"; + if (!_definitions.TryGetValue(identifierToUse, out var def)) { - def = new RequestDefinition(path, method) + def = new RequestDefinition(baseAddress, path, method) { Authenticated = authenticated, LimitGuard = limitGuard, @@ -110,9 +84,9 @@ namespace CryptoExchange.Net.Objects ParameterPosition = parameterPosition, PreventCaching = preventCaching ?? false, TryParseOnNonSuccess = tryParseOnNonSuccess ?? false, - ForcePathEndWithSlash = forcePathEndWithSlash ?? false + ForcePathEndWithSlash = forcePathEndWithSlash ?? false, }; - _definitions.TryAdd(identifier, def); + _definitions.TryAdd(identifierToUse, def); } return def; diff --git a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs index 1d981432..d98a60a9 100644 --- a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs +++ b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using CryptoExchange.Net.Interfaces; +using System.Collections.Generic; using System.Net.Http; namespace CryptoExchange.Net.Objects @@ -12,29 +13,17 @@ namespace CryptoExchange.Net.Objects private string? _queryString; /// - /// Http method + /// The request definition for the request /// - public HttpMethod Method { get; set; } - /// - /// Whether the request needs authentication - /// - public bool Authenticated { get; set; } - /// - /// Base address for the request - /// - public string BaseAddress { get; set; } - /// - /// The request path - /// - public string Path { get; set; } + public RequestDefinition RequestDefinition { get; set; } /// /// Query parameters /// - public IDictionary? QueryParameters { get; set; } + public Parameters? QueryParameters { get; set; } /// /// Body parameters /// - public IDictionary? BodyParameters { get; set; } + public Parameters? BodyParameters { get; set; } /// /// Request headers /// @@ -57,22 +46,16 @@ namespace CryptoExchange.Net.Objects /// public RestRequestConfiguration( RequestDefinition requestDefinition, - string baseAddress, - IDictionary? queryParams, - IDictionary? bodyParams, + Parameters? queryParams, + Parameters? bodyParams, IDictionary? headers, - ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parametersPosition, RequestBodyFormat bodyFormat) { - Method = requestDefinition.Method; - Authenticated = requestDefinition.Authenticated; - Path = requestDefinition.Path; - BaseAddress = baseAddress; + RequestDefinition = requestDefinition; QueryParameters = queryParams; BodyParameters = bodyParams; Headers = headers; - ArraySerialization = arraySerialization; ParameterPosition = parametersPosition; BodyFormat = bodyFormat; } @@ -80,15 +63,15 @@ namespace CryptoExchange.Net.Objects /// /// Get the parameter collection based on the ParameterPosition /// - public IDictionary GetPositionParameters() + public Parameters GetPositionParameters() { if (ParameterPosition == HttpMethodParameterPosition.InBody) { - BodyParameters ??= new Dictionary(); + BodyParameters ??= new Parameters(ParameterSerializationSettings.Default); return BodyParameters; } - QueryParameters ??= new Dictionary(); + QueryParameters ??= new Parameters(ParameterSerializationSettings.Default); return QueryParameters; } diff --git a/CryptoExchange.Net/Objects/Results/CallResult.cs b/CryptoExchange.Net/Objects/Results/CallResult.cs new file mode 100644 index 00000000..ff57d4d0 --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/CallResult.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace CryptoExchange.Net.Objects; + +/// +/// Call result +/// +public record CallResult : ICallResult +{ + private static CallResult _successResult = new CallResult(); + + /// + public Error? Error { get; init; } + /// + [MemberNotNullWhen(false, nameof(Error))] + public bool Success => Error == null; + + /// + /// Create an error response + /// + /// The error + public static CallResult Fail(Error error) => new CallResult { Error = error }; + /// + /// Create a success result + /// + public static CallResult Ok() => _successResult; + /// + /// Create a success result + /// + /// Result type + /// The original string data + /// Data type + public static CallResult Ok(T data, string? originalData = null) => new CallResult { Data = data, OriginalData = originalData }; + /// + /// Create an error response + /// + /// Result type + /// The original string data + /// The error + public static CallResult Fail(Error error, string? originalData = null) => new CallResult { Error = error, OriginalData = originalData }; + + /// + public override string ToString() + { + return Success ? $"Success" : $"Error: {Error}"; + } +} + + +/// +public record CallResult : CallResult, ICallResult +{ + /// + public new Error? Error + { + get => base.Error; + init => base.Error = value; + } + /// + [MemberNotNullWhen(false, nameof(Error))] + [MemberNotNullWhen(true, nameof(Data))] + public new bool Success => Error == null; + + /// + /// The data returned by the call, only available when Success = true + /// + public T? Data { get; init; } + + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + public string? OriginalData { get; init; } + + /// + /// Create an error response + /// + /// The error + /// The original string data + public static CallResult Fail(Error error, string? originalData = null) => new CallResult { Error = error, OriginalData = originalData }; + /// + /// Create a success result + /// + /// The data + /// The original string data + /// + public static CallResult Ok(T data, string? originalData = null) => new CallResult { Data = data, OriginalData = originalData }; +} + +/// +/// Call result for an exchange +/// +/// Data type +public record ExchangeCallResult : CallResult +{ + /// + /// Exchange name + /// + public string Exchange { get; set; } = string.Empty; + /// + /// Create an error response + /// + /// The exchange name + /// The error + /// The original string data + public static ExchangeCallResult Fail(string exchange, Error error, string? originalData = null) => new ExchangeCallResult { Exchange = exchange, Error = error }; + /// + /// Create a success result + /// + /// The exchange name + /// The data + /// The original string data + /// + public static ExchangeCallResult Ok(string exchange, T data, string? originalData = null) => new ExchangeCallResult { Exchange = exchange, Data = data }; +} \ No newline at end of file diff --git a/CryptoExchange.Net/Objects/Results/HttpResult.cs b/CryptoExchange.Net/Objects/Results/HttpResult.cs new file mode 100644 index 00000000..b5d32fec --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/HttpResult.cs @@ -0,0 +1,286 @@ +using CryptoExchange.Net.SharedApis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace CryptoExchange.Net.Objects; + +/// +/// HTTP call result +/// +public record HttpResult : IHttpResult +{ + /// + /// Create a new success HTTP result + /// + public static HttpResult Ok( + string exchange, + HttpStatusCode code, + Version version, + HttpResponseHeaders responseHeaders, + TimeSpan elapsed, + long? contentLength, + string? originalData, + int requestId, + string uri, + string? content, + HttpMethod method, + HttpRequestHeaders requestHeaders, + ResultDataSource source, + T data) => + new HttpResult(exchange, data, null) + { + ResponseStatusCode = code, + HttpVersion = version, + ResponseHeaders = responseHeaders, + ResponseTime = elapsed, + ResponseLength = contentLength, + OriginalData = originalData, + RequestId = requestId, + RequestUrl = uri, + RequestBody = content, + RequestMethod = method, + RequestHeaders = requestHeaders, + DataSource = source, + }; + + /// + /// Create a new success HTTP result + /// + public static HttpResult Ok(IHttpResult result, T data, PageRequest? pageRequest = null) => + new HttpResult(result.Exchange, data, null) + { + ResponseStatusCode = result.ResponseStatusCode, + HttpVersion = result.HttpVersion, + ResponseHeaders = result.ResponseHeaders, + ResponseTime = result.ResponseTime, + ResponseLength = result.ResponseLength, + OriginalData = result.OriginalData, + RequestId = result.RequestId, + RequestUrl = result.RequestUrl, + RequestBody = result.RequestBody, + RequestMethod = result.RequestMethod, + RequestHeaders = result.RequestHeaders, + DataSource = result.DataSource, + Error = result.Error, + Data = data, + NextPageRequest = pageRequest + }; + + /// + /// Create a new error HTTP result + /// + public static HttpResult Fail(string exchange, Error error) => new HttpResult(exchange, default, error); + + /// + /// Create a new error HTTP result + /// + public static HttpResult Fail(IHttpResult result, Error? error = null, T? data = default) + => new HttpResult(result.Exchange, data, error ?? result.Error) + { + ResponseStatusCode = result.ResponseStatusCode, + HttpVersion = result.HttpVersion, + ResponseHeaders = result.ResponseHeaders, + ResponseTime = result.ResponseTime, + ResponseLength = result.ResponseLength, + OriginalData = result.OriginalData, + RequestId = result.RequestId, + RequestUrl = result.RequestUrl, + RequestBody = result.RequestBody, + RequestMethod = result.RequestMethod, + RequestHeaders = result.RequestHeaders, + DataSource = result.DataSource, + }; + + /// + /// Create a new error HTTP result + /// + public static HttpResult Fail( + string exchange, + HttpStatusCode? code, + Version? version, + HttpResponseHeaders? responseHeaders, + TimeSpan elapsed, + long? contentLength, + string? originalData, + int requestId, + string uri, + string? content, + HttpMethod method, + HttpRequestHeaders requestHeaders, + ResultDataSource source, + Error error, + T? result = default) => + new HttpResult(exchange, result, error) + { + ResponseStatusCode = code, + HttpVersion = version, + ResponseHeaders = responseHeaders, + ResponseTime = elapsed, + ResponseLength = contentLength, + OriginalData = originalData, + RequestId = requestId, + RequestUrl = uri, + RequestBody = content, + RequestMethod = method, + RequestHeaders = requestHeaders, + DataSource = source, + }; + + /// + /// Create a new error HTTP result + /// + public static HttpResult Fail(string exchange, Error error) => new HttpResult() { Exchange = exchange, Error = error }; + + /// + /// Create a new error HTTP result + /// + public static HttpResult Fail(IHttpResult result, Error? error = null) + => new HttpResult() + { + ResponseStatusCode = result.ResponseStatusCode, + HttpVersion = result.HttpVersion, + ResponseHeaders = result.ResponseHeaders, + ResponseTime = result.ResponseTime, + ResponseLength = result.ResponseLength, + OriginalData = result.OriginalData, + RequestId = result.RequestId, + RequestUrl = result.RequestUrl, + RequestBody = result.RequestBody, + RequestMethod = result.RequestMethod, + RequestHeaders = result.RequestHeaders, + DataSource = result.DataSource, + Exchange = result.Exchange, + Error = error ?? result.Error + }; + + /// + /// Create a new success HTTP result + /// + public static HttpResult Ok(IHttpResult result) + => new HttpResult() + { + ResponseStatusCode = result.ResponseStatusCode, + HttpVersion = result.HttpVersion, + ResponseHeaders = result.ResponseHeaders, + ResponseTime = result.ResponseTime, + ResponseLength = result.ResponseLength, + OriginalData = result.OriginalData, + RequestId = result.RequestId, + RequestUrl = result.RequestUrl, + RequestBody = result.RequestBody, + RequestMethod = result.RequestMethod, + RequestHeaders = result.RequestHeaders, + DataSource = result.DataSource, + Exchange = result.Exchange, + }; + + + /// + /// Exchange name + /// + public string Exchange { get; init; } = string.Empty; + + /// + public Error? Error { get; internal set; } + /// + [MemberNotNullWhen(false, nameof(Error))] + public bool Success => Error == null; + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + public string? OriginalData { get; init; } + /// + /// The request http method + /// + public HttpMethod? RequestMethod { get; init; } + + /// + /// HTTP protocol version + /// + public Version? HttpVersion { get; init; } + + /// + /// The headers sent with the request + /// + public HttpRequestHeaders? RequestHeaders { get; init; } + + /// + /// The request id + /// + public int? RequestId { get; init; } + + /// + /// The url which was requested + /// + public string? RequestUrl { get; init; } + + /// + /// The body of the request + /// + public string? RequestBody { get; init; } + + /// + /// Length in bytes of the response + /// + public long? ResponseLength { get; init; } + + /// + /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. + /// + public HttpStatusCode? ResponseStatusCode { get; init; } + + /// + /// The response headers + /// + public HttpResponseHeaders? ResponseHeaders { get; init; } + + /// + /// The time between sending the request and receiving the response + /// + public TimeSpan? ResponseTime { get; init; } + /// + /// The data source of this result + /// + public ResultDataSource DataSource { get; init; } = ResultDataSource.Server; +} + + +/// +public record HttpResult : HttpResult, IHttpResult +{ + /// + /// ctor + /// + public HttpResult(string exchange, T? value, Error? error) + { + Exchange = exchange; + Data = value; + Error = error; + } + + /// + public new Error? Error + { + get => base.Error; + internal set => base.Error = value; + } + /// + [MemberNotNullWhen(false, nameof(Error))] + [MemberNotNullWhen(true, nameof(Data))] + public new bool Success => Error == null; + + /// + /// The data returned by the call, only available when Success = true + /// + public T? Data { get; init; } + + /// + /// Next page request, only potentially available when using Shared API's + /// + public PageRequest? NextPageRequest { get; init; } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Objects/Results/ICallResult.cs b/CryptoExchange.Net/Objects/Results/ICallResult.cs new file mode 100644 index 00000000..8df43ec6 --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/ICallResult.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace CryptoExchange.Net.Objects; + +/// +/// Call result +/// +public interface ICallResult +{ + /// + /// An error if the call didn't succeed, will always be filled if Success = false + /// + Error? Error { get; } + + /// + /// Whether the call was successful + /// + [MemberNotNullWhen(false, nameof(Error))] + bool Success { get; } +} + +/// +/// Call result +/// +/// Result data type +public interface ICallResult : ICallResult +{ + /// + new Error? Error { get; } + + /// + [MemberNotNullWhen(false, nameof(Error))] + [MemberNotNullWhen(true, nameof(Data))] + new bool Success { get; } + + /// + /// The result data, only available when Success = true + /// + T? Data { get; } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Objects/Results/IHttpResult.cs b/CryptoExchange.Net/Objects/Results/IHttpResult.cs new file mode 100644 index 00000000..f462521a --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/IHttpResult.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace CryptoExchange.Net.Objects +{ + /// + /// HTTP call result + /// + public interface IHttpResult : ICallResult + { + /// + /// Exchange name + /// + string Exchange { get; init; } + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + string? OriginalData { get; init; } + /// + /// The request http method + /// + HttpMethod? RequestMethod { get; init; } + + /// + /// HTTP protocol version + /// + Version? HttpVersion { get; init; } + + /// + /// The headers sent with the request + /// + HttpRequestHeaders? RequestHeaders { get; init; } + + /// + /// The request id + /// + int? RequestId { get; init; } + + /// + /// The url which was requested + /// + string? RequestUrl { get; init; } + + /// + /// The body of the request + /// + string? RequestBody { get; init; } + + /// + /// Length in bytes of the response + /// + long? ResponseLength { get; init; } + + /// + /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. + /// + HttpStatusCode? ResponseStatusCode { get; init; } + + /// + /// The response headers + /// + HttpResponseHeaders? ResponseHeaders { get; init; } + + /// + /// The time between sending the request and receiving the response + /// + TimeSpan? ResponseTime { get; init; } + /// + /// The data source of this result + /// + ResultDataSource DataSource { get; init; } + } + + /// + /// HTTP call result + /// + /// Result data type + public interface IHttpResult : IHttpResult, ICallResult + { + } +} diff --git a/CryptoExchange.Net/Objects/Results/IWebSocketResult.cs b/CryptoExchange.Net/Objects/Results/IWebSocketResult.cs new file mode 100644 index 00000000..bdd8d900 --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/IWebSocketResult.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace CryptoExchange.Net.Objects +{ + /// + /// WebSocket call result + /// + public interface IWebSocketResult : ICallResult + { + /// + /// Exchange name + /// + string Exchange { get; init; } + + /// + /// The request id + /// + public int? RequestId { get; init; } + + /// + /// The url which was requested + /// + public int? ConnectionId { get; init; } + + /// + /// The websocket url + /// + public string? Url { get; init; } + + /// + /// The time between sending the request and receiving the response + /// + public TimeSpan? ResponseTime { get; init; } + } + + /// + /// WebSocket call result + /// + /// Data result type + public interface IWebSocketResult : IWebSocketResult, ICallResult + { + + } + + /// + /// Query result + /// + public interface IQueryResult : IWebSocketResult + { + /// + /// The original returned data, only available when OutputOriginalData is set to true in the client options + /// + public string? OriginalData { get; init; } + /// + /// The query request body + /// + public string? RequestBody { get; init; } + } + + /// + /// Query result + /// + /// + public interface IQueryResult : IQueryResult, IWebSocketResult + { + } +} diff --git a/CryptoExchange.Net/Objects/Results/Unit.cs b/CryptoExchange.Net/Objects/Results/Unit.cs new file mode 100644 index 00000000..01ee7c08 --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/Unit.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects; + +/// +/// Void result +/// +public readonly struct Unit +{ + /// + /// Void value + /// + public static readonly Unit Value = default; + /// + /// Type + /// + public static Type Type { get; } = typeof(Unit); +} \ No newline at end of file diff --git a/CryptoExchange.Net/Objects/Results/WebSocketResult.cs b/CryptoExchange.Net/Objects/Results/WebSocketResult.cs new file mode 100644 index 00000000..d841399c --- /dev/null +++ b/CryptoExchange.Net/Objects/Results/WebSocketResult.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace CryptoExchange.Net.Objects; + +/// +/// WebSocket call result +/// +public record WebSocketResult : IWebSocketResult +{ + /// + /// ctor + /// + public WebSocketResult(string exchange, Error? error) + { + Exchange = exchange; + Error = error; + } + + /// + /// Create a new success WebSocket result + /// + public static WebSocketResult Ok(IWebSocketResult result, T data) => + new WebSocketResult(result.Exchange, data, null) + { + ConnectionId = result.ConnectionId, + Url = result.Url, + RequestId = result.RequestId, + ResponseTime = result.ResponseTime, + Error = result.Error, + Data = data + }; + + /// + /// Create a new success WebSocket result + /// + public static WebSocketResult Ok( + string exchange, + int connectionId, + TimeSpan elapsed, + int requestId, + string? url, + T data) => + new WebSocketResult(exchange, data, null) + { + ResponseTime = elapsed, + RequestId = requestId, + ConnectionId = connectionId, + Url = url + }; + + /// + /// Create a new success WebSocket result + /// + public static WebSocketResult Ok( + string exchange, + int connectionId, + TimeSpan elapsed, + int requestId, + string? url) => + new WebSocketResult(exchange, null) + { + ResponseTime = elapsed, + RequestId = requestId, + ConnectionId = connectionId, + Url = url + }; + + /// + /// Create a new error WebSocket result + /// + public static WebSocketResult Fail(string exchange, Error error) => new WebSocketResult(exchange, error); + /// + /// Create a new error WebSocket result + /// + public static WebSocketResult Fail( + string exchange, + int connectionId, + TimeSpan elapsed, + int requestId, + string? url, + Error error) => + new WebSocketResult(exchange, error) + { + ResponseTime = elapsed, + RequestId = requestId, + ConnectionId = connectionId, + Url = url + }; + /// + /// Create a new error WebSocket result + /// + public static WebSocketResult Fail(IWebSocketResult result, Error? error = null, T? data = default) + => new WebSocketResult(result.Exchange, data, error ?? result.Error) + { + ConnectionId = result.ConnectionId, + Url = result.Url, + RequestId = result.RequestId, + ResponseTime = result.ResponseTime, + }; + /// + /// Create a new error WebSocket result + /// + public static WebSocketResult Fail(string exchange, Error error) => new WebSocketResult(exchange, default, error); + + /// + /// Create a new error WebSocket result + /// + public static WebSocketResult Fail( + string exchange, + int connectionId, + TimeSpan elapsed, + int requestId, + string? url, + Error error) => + new WebSocketResult(exchange, default, error) + { + ResponseTime = elapsed, + RequestId = requestId, + ConnectionId = connectionId, + Url = url + }; + + + /// + /// Exchange name + /// + public string Exchange { get; init; } + /// + public Error? Error { get; init; } + /// + [MemberNotNullWhen(false, nameof(Error))] + public bool Success => Error == null; + + /// + /// The request id + /// + public int? RequestId { get; init; } + + /// + /// The url which was requested + /// + public int? ConnectionId { get; init; } + + /// + /// The websocket url + /// + public string? Url { get; init; } + + /// + /// The time between sending the request and receiving the response + /// + public TimeSpan? ResponseTime { get; init; } +} + +/// +public record WebSocketResult : WebSocketResult, IWebSocketResult +{ + /// + /// ctor + /// + public WebSocketResult(string exchange, T? value, Error? error): base(exchange, error) + { + Data = value; + } + + /// + public new Error? Error + { + get => base.Error; + init => base.Error = value; + } + /// + [MemberNotNullWhen(false, nameof(Error))] + [MemberNotNullWhen(true, nameof(Data))] + public new bool Success => Error == null; + /// + /// The data returned by the call, only available when Success = true + /// + public T? Data { get; init; } +} + +/// +public record QueryResult : WebSocketResult +{ + /// + /// ctor + /// + public QueryResult(string exchange, Error? error) : base(exchange, error) + { + } + + /// + /// Create a new error Query result + /// + public new static QueryResult Fail(string exchange, Error error) => new QueryResult(exchange, error); + + + /// + /// Create a new error WebSocket result + /// + public static QueryResult Fail(IQueryResult result, Error? error = null) + => new QueryResult(result.Exchange, error ?? result.Error) + { + ConnectionId = result.ConnectionId, + Url = result.Url, + RequestId = result.RequestId, + ResponseTime = result.ResponseTime, + }; + + /// + /// Create a new success query result + /// + public static QueryResult Ok( + string exchange, + int connectionId, + TimeSpan elapsed, + int requestId, + string? requestBody, + string? url, + string? originalData, + T data) => + new QueryResult(exchange, data, null) + { + ResponseTime = elapsed, + RequestId = requestId, + RequestBody = requestBody, + ConnectionId = connectionId, + Url = url, + OriginalData = originalData, + }; + /// + /// Create a new success WebSocket result + /// + public static QueryResult Ok(IQueryResult result, T data) => + new QueryResult(result.Exchange, data, null) + { + ConnectionId = result.ConnectionId, + Url = result.Url, + RequestId = result.RequestId, + RequestBody = result.RequestBody, + ResponseTime = result.ResponseTime, + Error = result.Error, + OriginalData = result.OriginalData, + Data = data + }; + + /// + /// Create a new error WebSocket result + /// + public static QueryResult Fail( + string exchange, + int connectionId, + TimeSpan elapsed, + int requestId, + string? requestBody, + string? url, + string? originalData, + Error error) => + new QueryResult(exchange, default, error) + { + ResponseTime = elapsed, + RequestId = requestId, + RequestBody = requestBody, + ConnectionId = connectionId, + OriginalData = originalData, + Url = url + }; + /// + /// Create a new error WebSocket result + /// + public static QueryResult Fail(IQueryResult result, Error? error = null, T? data = default) + => new QueryResult(result.Exchange, data, error ?? result.Error) + { + ConnectionId = result.ConnectionId, + Url = result.Url, + RequestId = result.RequestId, + RequestBody = result.RequestBody, + OriginalData = result.OriginalData, + ResponseTime = result.ResponseTime, + }; + /// + /// Create a new error WebSocket result + /// + public new static QueryResult Fail(string exchange, Error error) => new QueryResult(exchange, default, error); + + /// + public string? RequestBody { get; init; } +} + +/// +public record QueryResult : QueryResult, IQueryResult +{ + /// + /// ctor + /// + public QueryResult(string exchange, T? value, Error? error) : base(exchange, error) + { + Data = value; + } + + + /// + public new Error? Error + { + get => base.Error; + init => base.Error = value; + } + /// + [MemberNotNullWhen(false, nameof(Error))] + [MemberNotNullWhen(true, nameof(Data))] + public new bool Success => Error == null; + /// + public T? Data { get; set; } + + /// + public string? OriginalData { get; init; } +} \ No newline at end of file diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 5c646b9b..9ead7c56 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -262,7 +262,7 @@ namespace CryptoExchange.Net.OrderBook } /// - public async Task> StartAsync(CancellationToken? ct = null) + public async Task StartAsync(CancellationToken? ct = null) { if (Status != OrderBookStatus.Disconnected) throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}"); @@ -286,10 +286,10 @@ namespace CryptoExchange.Net.OrderBook _processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning); var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false); - if (!startResult) + if (!startResult.Success) { Status = OrderBookStatus.Disconnected; - return new CallResult(startResult.Error!); + return CallResult.Fail(startResult.Error!); } if (_cts.IsCancellationRequested) @@ -297,7 +297,7 @@ namespace CryptoExchange.Net.OrderBook _logger.OrderBookStoppedStarting(Api, Symbol); await startResult.Data.CloseAsync().ConfigureAwait(false); Status = OrderBookStatus.Disconnected; - return new CallResult(new CancellationRequestedError()); + return CallResult.Fail(new CancellationRequestedError()); } _subscription = startResult.Data; @@ -306,7 +306,7 @@ namespace CryptoExchange.Net.OrderBook _subscription.ConnectionRestored += HandleConnectionRestored; Status = OrderBookStatus.Synced; - return new CallResult(true); + return CallResult.Ok(); } private void HandleConnectionLost() @@ -354,7 +354,7 @@ namespace CryptoExchange.Net.OrderBook public CallResult CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type) { if (Status != OrderBookStatus.Synced) - return new CallResult(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); + return CallResult.Fail(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); var totalCost = 0m; var totalAmount = 0m; @@ -367,7 +367,7 @@ namespace CryptoExchange.Net.OrderBook while (amountLeft > 0) { if (step == list.Count) - return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); + return CallResult.Fail(new InvalidOperationError("Quantity is larger than order in the order book")); var element = list.ElementAt(step); var stepAmount = Math.Min(element.Value.Quantity, amountLeft); @@ -378,14 +378,14 @@ namespace CryptoExchange.Net.OrderBook } } - return new CallResult(Math.Round(totalCost / totalAmount, 8)); + return CallResult.Ok(Math.Round(totalCost / totalAmount, 8)); } /// public CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type) { if (Status != OrderBookStatus.Synced) - return new CallResult(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state")); + return CallResult.Fail(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state")); var quoteQuantityLeft = quoteQuantity; var totalBaseQuantity = 0m; @@ -397,7 +397,7 @@ namespace CryptoExchange.Net.OrderBook while (quoteQuantityLeft > 0) { if (step == list.Count) - return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); + return CallResult.Fail(new InvalidOperationError("Quantity is larger than order in the order book")); var element = list.ElementAt(step); var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft); @@ -407,7 +407,7 @@ namespace CryptoExchange.Net.OrderBook } } - return new CallResult(Math.Round(totalBaseQuantity, 8)); + return CallResult.Ok(Math.Round(totalBaseQuantity, 8)); } /// @@ -426,7 +426,7 @@ namespace CryptoExchange.Net.OrderBook /// Resync the order book /// /// - protected abstract Task> DoResyncAsync(CancellationToken ct); + protected abstract Task DoResyncAsync(CancellationToken ct); /// /// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false) @@ -605,10 +605,9 @@ namespace CryptoExchange.Net.OrderBook var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids; if (entry.Quantity == 0) { - if (!listToChange.ContainsKey(entry.Price)) + if (!listToChange.Remove(entry.Price)) return true; - listToChange.Remove(entry.Price); if (type == OrderBookEntryType.Ask) AskCount--; else BidCount--; } @@ -635,16 +634,16 @@ namespace CryptoExchange.Net.OrderBook /// Max wait time /// Cancellation token /// - protected async Task> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct) + protected async Task WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct) { var startWait = DateTime.UtcNow; while (!_bookSet && Status == OrderBookStatus.Syncing) { if(ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); + return CallResult.Fail(new CancellationRequestedError()); if (DateTime.UtcNow - startWait > timeout) - return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); + return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); try { @@ -654,7 +653,7 @@ namespace CryptoExchange.Net.OrderBook { } } - return new CallResult(true); + return CallResult.Ok(); } /// @@ -670,10 +669,10 @@ namespace CryptoExchange.Net.OrderBook while (_processBuffer.Count == 0) { if (ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); + return CallResult.Fail(new CancellationRequestedError()); if (DateTime.UtcNow - startWait > maxWait) - return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); + return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); try { @@ -690,7 +689,7 @@ namespace CryptoExchange.Net.OrderBook await Task.Delay(minWait.Value - dif).ConfigureAwait(false); } - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -809,7 +808,7 @@ namespace CryptoExchange.Net.OrderBook return; var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false); - success = resyncResult; + success = resyncResult.Success; } _logger.OrderBookResynced(Api, Symbol); @@ -835,7 +834,7 @@ namespace CryptoExchange.Net.OrderBook if (item is OrderBookSnapshot snapshot) ProcessOrderBookSnapshot(snapshot); - if (item is OrderBookUpdate update) + else if (item is OrderBookUpdate update) ProcessQueueItem(update); else if (item is OrderBookChecksum checksum) ProcessChecksum(checksum); @@ -963,7 +962,8 @@ namespace CryptoExchange.Net.OrderBook await _subscription!.UnsubscribeAsync().ConfigureAwait(false); Reset(); _stopProcessing = false; - if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false)) + var resubResult = await _subscription!.ResubscribeAsync().ConfigureAwait(false); + if (!resubResult.Success) { // Resubscribing failed, reconnect the socket _logger.OrderBookResyncFailed(Api, Symbol); @@ -1055,10 +1055,12 @@ namespace CryptoExchange.Net.OrderBook private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber) { - if (sequenceNumber < LastSequenceNumber + if (sequenceNumber < LastSequenceNumber && (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet)) + { // Update is somehow from before the current state return SequenceNumberResult.OutOfSync; + } if (_sequencesAreConsecutive && LastSequenceNumber != 0 diff --git a/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs index b40b36f3..aa475bdc 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs @@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters } /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey) => definition.Authenticated == _authenticated; } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs index bf8681ed..9c32815b 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters } /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey) => string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase); } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs index 6f663775..20f86c5c 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters } /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey) => _paths.Contains(definition.Path); } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs index 4a6dc9fb..8c54d6c6 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs @@ -20,8 +20,8 @@ namespace CryptoExchange.Net.RateLimiting.Filters } /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => host.Equals(_host, System.StringComparison.InvariantCulture); + public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey) + => definition.BaseAddress.Equals(_host, System.StringComparison.InvariantCulture); } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs index 93137a54..dc815f1f 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs @@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters } /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey) => type == _type; } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs index 1001b663..31e88dec 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters } /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey) => definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase); } } diff --git a/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs index 206de601..4ef0ca09 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs @@ -14,30 +14,30 @@ namespace CryptoExchange.Net.RateLimiting.Guards /// /// Apply guard per host /// - public static Func PerHost { get; } = new Func((def, host, key) => host); + public static Func PerHost { get; } = new Func((def, key) => def.BaseAddress); /// /// Apply guard per endpoint /// - public static Func PerEndpoint { get; } = new Func((def, host, key) => def.Path + def.Method); + public static Func PerEndpoint { get; } = new Func((def, key) => def.Path + def.Method); /// /// Apply guard per connection /// - public static Func PerConnection { get; } = new Func((def, host, key) => def.ConnectionId.ToString()!); + public static Func PerConnection { get; } = new Func((def, key) => def.ConnectionId.ToString()!); /// /// Apply guard per API key /// - public static Func PerApiKey { get; } = new Func((def, host, key) => key!); + public static Func PerApiKey { get; } = new Func((def, key) => key!); /// /// Apply guard per API key per endpoint /// - public static Func PerApiKeyPerEndpoint { get; } = new Func((def, host, key) => key! + def.Path + def.Method); + public static Func PerApiKeyPerEndpoint { get; } = new Func((def, key) => key! + def.Path + def.Method); private readonly IEnumerable _filters; private readonly Dictionary _trackers; private readonly RateLimitWindowType _windowType; private readonly double? _decayRate; private readonly int? _connectionWeight; - private readonly Func _keySelector; + private readonly Func _keySelector; private readonly SemaphoreSlim? _sharedGuardSemaphore; /// @@ -71,7 +71,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards /// The decay per timespan if windowType is DecayWindowTracker /// The weight of a new connection /// Whether this guard is shared between multiple gates - public RateLimitGuard(Func keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false) + public RateLimitGuard(Func keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false) : this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared) { } @@ -87,7 +87,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards /// The decay per timespan if windowType is DecayWindowTracker /// The weight of a new connection /// Whether this guard is shared between multiple gates - public RateLimitGuard(Func keySelector, IEnumerable filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false) + public RateLimitGuard(Func keySelector, IEnumerable filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false) { _filters = filters; _trackers = new Dictionary(); @@ -104,11 +104,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) { foreach (var filter in _filters) { - if (!filter.Passes(type, definition, host, apiKey)) + if (!filter.Passes(type, definition, apiKey)) return LimitCheck.NotApplicable; } @@ -120,7 +120,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards try { - var key = _keySelector(definition, host, apiKey) + keySuffix; + var key = _keySelector(definition, apiKey) + keySuffix; if (!_trackers.TryGetValue(key, out var tracker)) { tracker = CreateTracker(); @@ -141,11 +141,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) { foreach (var filter in _filters) { - if (!filter.Passes(type, definition, host, apiKey)) + if (!filter.Passes(type, definition, apiKey)) return RateLimitState.NotApplied; } @@ -153,7 +153,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards requestWeight = _connectionWeight ?? requestWeight; - var key = _keySelector(definition, host, apiKey) + keySuffix; + var key = _keySelector(definition, apiKey) + keySuffix; var tracker = _trackers[key]; if (SharedGuard) @@ -173,11 +173,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix) + public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount) { foreach (var filter in _filters) { - if (!filter.Passes(type, definition, host, apiKey)) + if (!filter.Passes(type, definition, apiKey)) return; } @@ -186,11 +186,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards try { - var key = _keySelector(definition, host, apiKey) + keySuffix; + var key = _keySelector(definition, apiKey) + keySuffix; if (!_trackers.TryGetValue(key, out var tracker)) return; - tracker.Reset(); + tracker.Reset(amount); } finally { diff --git a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs index 1b4fe6d8..c6116813 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs @@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) { if (type != Type) return LimitCheck.NotApplicable; @@ -55,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) { return RateLimitState.NotApplied; } @@ -67,7 +67,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards public void UpdateAfter(DateTime after) => After = after; /// - public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix) + public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount) { After = DateTime.UtcNow; } diff --git a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs index 6f35f2d0..adb60fc3 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs @@ -14,19 +14,19 @@ namespace CryptoExchange.Net.RateLimiting.Guards /// /// Default endpoint limit /// - public static Func Default { get; } = new Func((def, host, key) => def.Path + def.Method); + public static Func Default { get; } = new Func((def, key) => def.Path + def.Method); /// /// Endpoint limit per API key /// - public static Func PerApiKey { get; } = new Func((def, host, key) => def.Path + def.Method + key); + public static Func PerApiKey { get; } = new Func((def, key) => def.Path + def.Method + key); private readonly Dictionary _trackers; private readonly RateLimitWindowType _windowType; private readonly double? _decayRate; private readonly int _limit; private readonly TimeSpan _period; - private readonly Func _keySelector; + private readonly Func _keySelector; /// public string Name => "EndpointLimitGuard"; @@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards TimeSpan period, RateLimitWindowType windowType, double? decayRate = null, - Func? keySelector = null) + Func? keySelector = null) { _limit = limit; _period = period; @@ -53,9 +53,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) { - var key = _keySelector(definition, host, apiKey) + keySuffix; + var key = _keySelector(definition, apiKey) + keySuffix; if (!_trackers.TryGetValue(key, out var tracker)) { tracker = CreateTracker(); @@ -70,9 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) { - var key = _keySelector(definition, host, apiKey) + keySuffix; + var key = _keySelector(definition, apiKey) + keySuffix; var tracker = _trackers[key]; tracker.ApplyWeight(requestWeight); return RateLimitState.Applied(_limit, _period, tracker.Current); @@ -90,13 +90,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix) + public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount) { - var key = _keySelector(definition, host, apiKey) + keySuffix; + var key = _keySelector(definition, apiKey) + keySuffix; if (!_trackers.TryGetValue(key, out var tracker)) return; - tracker.Reset(); + tracker.Reset(amount); } } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs index 8a75999e..5f1ceb92 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs @@ -12,9 +12,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// /// The type of item /// The request definition - /// The host address /// The API key /// True if passed - bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey); + bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey); } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs index 09f279ce..e0feafde 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs @@ -49,14 +49,13 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// Id of the item to check /// The rate limit item type /// The request definition - /// The host address /// The API key /// Request weight /// Behaviour when rate limit is hit /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// Cancelation token /// Error if RateLimitingBehaviour is Fail and rate limit is hit - ValueTask ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); + ValueTask ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); /// /// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail @@ -66,30 +65,29 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// The guard /// The rate limit item type /// The request definition - /// The host address /// The API key /// Behaviour when rate limit is hit /// The weight to apply to the limit guard /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// Cancelation token /// Error if RateLimitingBehaviour is Fail and rate limit is hit - ValueTask ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); + ValueTask ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); /// /// Reset the limit for the specified parameters /// /// The rate limit item type /// The request definition - /// The host address /// The API key /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// Amount in weight to reset by, or null to set used rate limit to 0 /// Cancelation token Task ResetAsync( RateLimitItemType type, RequestDefinition definition, - string host, string? apiKey, string? keySuffix, + int? amount, CancellationToken ct); } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs index 23124a19..ccfec758 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs @@ -22,33 +22,31 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// /// The rate limit item type /// The request definition - /// The host address /// The API key /// The request weight /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// - LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); + LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix); /// /// Apply the request to this guard with the specified weight /// /// The rate limit item type /// The request definition - /// The host address /// The API key /// The request weight /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// - RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); + RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix); /// /// Reset the limit for the specified parameters /// /// The rate limit item type /// The request definition - /// The host address /// The API key /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix); + /// Amount in weight to reset by, or null to set used rate limit to 0 + void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount); } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs index 1ddf927f..a61e6971 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs @@ -33,6 +33,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// /// Reset the limit counter for this tracker /// - void Reset(); + void Reset(int? amount); } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs b/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs index c79e03e9..08f7956a 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs @@ -25,10 +25,6 @@ namespace CryptoExchange.Net.RateLimiting /// public RequestDefinition RequestDefinition { get; set; } /// - /// The host the request is for - /// - public string Host { get; set; } = default!; - /// /// The current counter value /// public int Current { get; set; } @@ -56,13 +52,12 @@ namespace CryptoExchange.Net.RateLimiting /// /// ctor /// - public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour) + public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour) { ItemId = itemId; ApiLimit = apiLimit; LimitDescription = limitDescription; RequestDefinition = definition; - Host = host; Current = current; RequestWeight = requestWeight; Limit = limit; diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs index 74279544..f7b7ca7f 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs @@ -37,20 +37,20 @@ namespace CryptoExchange.Net.RateLimiting } /// - public async ValueTask ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + public async ValueTask ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) { await _semaphore.WaitAsync(ct).ConfigureAwait(false); bool release = true; _waitingCount++; try { - return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); + return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } catch (TaskCanceledException tce) { // The semaphore has already been released if the task was cancelled release = false; - return new CallResult(new CancellationRequestedError(tce)); + return CallResult.Fail(new CancellationRequestedError(tce)); } finally { @@ -67,7 +67,6 @@ namespace CryptoExchange.Net.RateLimiting IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, - string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, @@ -79,13 +78,13 @@ namespace CryptoExchange.Net.RateLimiting _waitingCount++; try { - return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); + return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } catch (TaskCanceledException tce) { // The semaphore has already been released if the task was cancelled release = false; - return new CallResult(new CancellationRequestedError(tce)); + return CallResult.Fail(new CancellationRequestedError(tce)); } finally { @@ -95,12 +94,12 @@ namespace CryptoExchange.Net.RateLimiting } } - private async ValueTask CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + private async ValueTask CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) { foreach (var guard in guards) { // Check if a wait is needed for this guard - var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix); + var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix); if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail) { // Delay is needed and limit behaviour is to fail the request @@ -109,8 +108,8 @@ namespace CryptoExchange.Net.RateLimiting else logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); - RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); - return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}")); + RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); + return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}")); } if (result.Delay != TimeSpan.Zero) @@ -124,17 +123,17 @@ namespace CryptoExchange.Net.RateLimiting else logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); - RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); + RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false); - return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); + return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } } // Apply the weight on each guard foreach (var guard in guards) { - var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix); + var result = guard.ApplyWeight(type, definition, apiKey, requestWeight, keySuffix); if (result.IsApplied) { RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period)); @@ -149,7 +148,7 @@ namespace CryptoExchange.Net.RateLimiting } } - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -198,16 +197,16 @@ namespace CryptoExchange.Net.RateLimiting public async Task ResetAsync( RateLimitItemType type, RequestDefinition definition, - string host, string? apiKey, string? keySuffix, + int? amount, CancellationToken ct) { await _semaphore.WaitAsync(ct).ConfigureAwait(false); try { foreach (var guard in _guards) - guard.Reset(type, definition, host, apiKey, keySuffix); + guard.Reset(type, definition, apiKey, keySuffix, amount); } finally { diff --git a/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs index 4201b806..09218ae4 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs @@ -27,10 +27,17 @@ namespace CryptoExchange.Net.RateLimiting.Trackers } /// - public void Reset() + public void Reset(int? amount) { - _currentWeight = 0; - _lastDecrease = DateTime.UtcNow; + if (amount == null) + { + _lastDecrease = DateTime.UtcNow; + _currentWeight = 0; + } + else + { + _currentWeight = Math.Max(0, _currentWeight - amount.Value); + } } /// diff --git a/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs index 34be9895..02e5d74c 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs @@ -30,11 +30,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers } /// - public void Reset() + public void Reset(int? amount) { - _entries.Clear(); - _currentWeight = 0; - _nextReset = null; + if (amount == null) + { + _entries.Clear(); + _currentWeight = 0; + _nextReset = null; + } + else + { + _currentWeight = Math.Max(0, _currentWeight - amount.Value); + var removedWeight = 0; + while (true) + { + if (removedWeight >= amount.Value || _entries.Count == 0) + break; + + var lastEntry = _entries.Dequeue(); + removedWeight += lastEntry.Weight; + } + } } public TimeSpan GetWaitTime(int weight) diff --git a/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs index 9b76e583..a45c68c2 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs @@ -29,10 +29,26 @@ namespace CryptoExchange.Net.RateLimiting.Trackers } /// - public void Reset() + public void Reset(int? amount) { - _entries.Clear(); - _currentWeight = 0; + if (amount == null) + { + _entries.Clear(); + _currentWeight = 0; + } + else + { + _currentWeight = Math.Max(0, _currentWeight - amount.Value); + var removedWeight = 0; + while (true) + { + if (removedWeight >= amount.Value || _entries.Count == 0) + break; + + var lastEntry = _entries.Dequeue(); + removedWeight += lastEntry.Weight; + } + } } /// diff --git a/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs index ea425472..c37ac23e 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs @@ -29,10 +29,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers } /// - public void Reset() + public void Reset(int? amount) { - _entries.Clear(); - _currentWeight = 0; + if (amount == null) + { + _entries.Clear(); + _currentWeight = 0; + } + else + { + _currentWeight = Math.Max(0, _currentWeight - amount.Value); + var removedWeight = 0; + while (true) + { + if (removedWeight >= amount.Value || _entries.Count == 0) + break; + + var lastEntry = _entries[_entries.Count - 1]; + removedWeight += lastEntry.Weight; + _entries.Remove(lastEntry); + } + } } /// diff --git a/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs index da7a1a46..67bc3c4c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs @@ -22,6 +22,11 @@ namespace CryptoExchange.Net.SharedApis /// bool Authenticated { get; } + /// + /// Get info on the client and supported features + /// + SharedClientInfo Discover(); + /// /// Format a base and quote asset to an exchange accepted symbol /// @@ -33,14 +38,15 @@ namespace CryptoExchange.Net.SharedApis string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); /// - /// Set a default exchange parameter. This can be used instead of passing in an ExchangeParameters object which each request. + /// Set a default exchange parameter which will be statically set with each request. This can be used instead of passing it in an ExchangeParameters object with each request.
+ /// Default exchange parameters can still be overridden by passing the parameter in the ExchangeParameters of a request. ///
/// Parameter name /// Parameter value void SetDefaultExchangeParameter(string name, object value); /// - /// Reset the default exchange parameters, resets parameters for all exchanges + /// Reset previously set default exchange parameters for the exchange. /// void ResetDefaultExchangeParameters(); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs index 0798554b..7e28a571 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IFundingRateRestClient : ISharedClient { /// - /// Funding rate request options + /// Funding rate request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; } /// - /// Get funding rate records + /// Get funding rate records, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs index 2f8e925f..80f0531b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using System.Threading; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.SharedApis { @@ -9,26 +10,30 @@ namespace CryptoExchange.Net.SharedApis public interface IFuturesOrderClientIdRestClient : ISharedClient { /// - /// Futures get order by client order id request options + /// Futures get order by client order id request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetFuturesOrderByClientOrderIdOptions { get; } + GetFuturesOrderByClientOrderIdOptions GetFuturesOrderByClientOrderIdOptions { get; } /// - /// Get info on a specific futures order using a client order id + /// Get info on a specific futures order using a client order id, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); + Task> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); /// - /// Futures cancel order by client order id request options + /// Futures cancel order by client order id request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelFuturesOrderByClientOrderIdOptions { get; } + CancelFuturesOrderByClientOrderIdOptions CancelFuturesOrderByClientOrderIdOptions { get; } /// - /// Cancel a futures order using client order id + /// Cancel a futures order using client order id, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); + Task> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs index 39632ddd..446cd665 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -16,17 +17,16 @@ namespace CryptoExchange.Net.SharedApis /// How the asset is determined in which the trading fee is paid /// SharedFeeAssetType FuturesFeeAssetType { get; } - /// - /// Supported order types + /// Supported order types for futures orders /// SharedOrderType[] FuturesSupportedOrderTypes { get; } /// - /// Supported time in force + /// Supported time in force types for futures orders /// SharedTimeInForce[] FuturesSupportedTimeInForce { get; } /// - /// Quantity types support + /// Supported quantity types for futures orders /// SharedQuantitySupport FuturesSupportedOrderQuantity { get; } @@ -37,106 +37,126 @@ namespace CryptoExchange.Net.SharedApis string GenerateClientOrderId(); /// - /// Futures place order request options + /// Futures place order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; } /// - /// Place a new futures order + /// Place a new futures order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default); + Task> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default); /// - /// Futures get order request options + /// Futures get order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetFuturesOrderOptions { get; } + GetFuturesOrderOptions GetFuturesOrderOptions { get; } /// - /// Get info on a specific futures order + /// Get info on a specific futures order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default); + Task> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default); /// - /// Futures get open orders request options + /// Futures get open orders request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetOpenFuturesOrdersOptions { get; } + GetOpenFuturesOrdersOptions GetOpenFuturesOrdersOptions { get; } /// - /// Get info on a open futures orders + /// Get info on a open futures orders, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); + Task> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); /// - /// Spot get closed orders request options + /// Spot get closed orders request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; } + GetFuturesClosedOrdersOptions GetClosedFuturesOrdersOptions { get; } /// - /// Get info on closed futures orders + /// Get info on closed futures orders, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// - /// Futures get order trades request options + /// Futures get order trades request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetFuturesOrderTradesOptions { get; } + GetFuturesOrderTradesOptions GetFuturesOrderTradesOptions { get; } /// - /// Get trades for a specific futures order + /// Get trades for a specific futures order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); + Task> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); /// - /// Futures user trades request options + /// Futures user trades request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetUserTradesOptions GetFuturesUserTradesOptions { get; } + GetFuturesUserTradesOptions GetFuturesUserTradesOptions { get; } /// - /// Get futures user trade records + /// Get futures user trade records, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// - /// Futures cancel order request options + /// Futures cancel order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelFuturesOrderOptions { get; } + CancelFuturesOrderOptions CancelFuturesOrderOptions { get; } /// - /// Cancel a futures order + /// Cancel a futures order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + Task> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default); /// - /// Positions request options + /// Positions request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetPositionsOptions { get; } + GetPositionsOptions GetPositionsOptions { get; } /// - /// Get open position info + /// Get open position info, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default); + Task> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default); /// - /// Close position order request options + /// Close position order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions ClosePositionOptions { get; } + ClosePositionOptions ClosePositionOptions { get; } /// - /// Close a currently open position + /// Close a currently open position, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> ClosePositionAsync(ClosePositionRequest request, CancellationToken ct = default); + Task> ClosePositionAsync(ClosePositionRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs index fa7e977b..761c2dcb 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,33 +10,35 @@ namespace CryptoExchange.Net.SharedApis public interface IFuturesSymbolRestClient : ISharedClient { /// - /// Futures symbol request options + /// Futures symbol request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetFuturesSymbolsOptions { get; } + GetFuturesSymbolsOptions GetFuturesSymbolsOptions { get; } /// /// Get all futures symbols for a specific base asset /// /// Asset, for example `ETH` - Task> GetFuturesSymbolsForBaseAssetAsync(string baseAsset); + Task> GetFuturesSymbolsForBaseAssetAsync(string baseAsset); /// /// Gets whether the client supports a futures symbol /// /// The symbol - Task> SupportsFuturesSymbolAsync(SharedSymbol symbol); + Task> SupportsFuturesSymbolAsync(SharedSymbol symbol); /// /// Gets whether the client supports a futures symbol /// /// The symbol name - Task> SupportsFuturesSymbolAsync(string symbolName); + Task> SupportsFuturesSymbolAsync(string symbolName); /// - /// Get info on all futures symbols supported on the exchange + /// Get info on all futures symbols supported on the exchange, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); + Task> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs index 8343139c..3ea6048f 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,25 +10,29 @@ namespace CryptoExchange.Net.SharedApis public interface IFuturesTickerRestClient : ISharedClient { /// - /// Futures get ticker request options + /// Futures get ticker request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetTickerOptions GetFuturesTickerOptions { get; } + GetFuturesTickerOptions GetFuturesTickerOptions { get; } /// - /// Get ticker info for a specific futures symbol + /// Get ticker info for a specific futures symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct = default); + Task> GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct = default); /// - /// Futures get tickers request options + /// Futures get tickers request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetTickersOptions GetFuturesTickersOptions { get; } + GetFuturesTickersOptions GetFuturesTickersOptions { get; } /// - /// Get ticker info for all futures symbols + /// Get ticker info for all futures symbols, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default); + Task> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs index 9f62e8bb..2069f80b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using System.Threading; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.SharedApis { @@ -9,27 +10,31 @@ namespace CryptoExchange.Net.SharedApis public interface IFuturesTpSlRestClient : ISharedClient { /// - /// Set take profit and/or stop loss options + /// Set take profit and/or stop loss options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions SetFuturesTpSlOptions { get; } + SetFuturesTpSlOptions SetFuturesTpSlOptions { get; } /// - /// Set a take profit and/or stop loss for an open position + /// Set a take profit and/or stop loss for an open position, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default); + Task> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default); /// - /// Cancel a take profit and/or stop loss options + /// Cancel a take profit and/or stop loss options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelFuturesTpSlOptions { get; } + CancelFuturesTpSlOptions CancelFuturesTpSlOptions { get; } /// - /// Cancel an active take profit and/or stop loss for an open position + /// Cancel an active take profit and/or stop loss for an open position, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default); + Task> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs index bdda1c81..67c396cc 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,39 +10,44 @@ namespace CryptoExchange.Net.SharedApis public interface IFuturesTriggerOrderRestClient : ISharedClient { /// - /// Place spot trigger order options + /// Place spot trigger order options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; } /// - /// Place a new trigger order + /// Place a new trigger order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default); - + Task> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default); /// - /// Get trigger order request options + /// Get trigger order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetFuturesTriggerOrderOptions { get; } + GetFuturesTriggerOrderOptions GetFuturesTriggerOrderOptions { get; } /// - /// Get info on a specific trigger order + /// Get info on a specific trigger order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); + Task> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); /// - /// Cancel trigger order request options + /// Cancel trigger order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelFuturesTriggerOrderOptions { get; } + CancelFuturesTriggerOrderOptions CancelFuturesTriggerOrderOptions { get; } /// - /// Cancel a trigger order + /// Cancel a trigger order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + Task> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs index 85b78a27..bd6c79e2 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IIndexPriceKlineRestClient : ISharedClient { /// - /// Index price klines request options + /// Index price klines request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetKlinesOptions GetIndexPriceKlinesOptions { get; } + GetIndexPriceKlinesOptions GetIndexPriceKlinesOptions { get; } /// - /// Get index price kline/candlestick data + /// Get index price kline/candlestick data, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs index 0262f697..148ce59c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -14,26 +15,30 @@ namespace CryptoExchange.Net.SharedApis SharedLeverageSettingMode LeverageSettingType { get; } /// - /// Leverage request options + /// Leverage request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetLeverageOptions { get; } + GetLeverageOptions GetLeverageOptions { get; } /// - /// Get the current leverage setting for a symbol + /// Get the current leverage setting for a symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default); + Task> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default); /// - /// Leverage set request options + /// Leverage set request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
SetLeverageOptions SetLeverageOptions { get; } /// - /// Set the leverage for a symbol + /// Set the leverage for a symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default); + Task> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs index a749c166..761bb167 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IMarkPriceKlineRestClient : ISharedClient { /// - /// Mark price klines request options + /// Mark price klines request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetKlinesOptions GetMarkPriceKlinesOptions { get; } + GetMarkPriceKlinesOptions GetMarkPriceKlinesOptions { get; } /// - /// Get mark price kline/candlestick data + /// Get mark price kline/candlestick data, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs index 1dac2c8a..3e9fca24 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,14 +10,16 @@ namespace CryptoExchange.Net.SharedApis public interface IOpenInterestRestClient : ISharedClient { /// - /// Open interest request options + /// Open interest request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetOpenInterestOptions { get; } + GetOpenInterestOptions GetOpenInterestOptions { get; } /// - /// Get the open interest for a symbol + /// Get the open interest for a symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default); + Task> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs index d51b8c2a..d446ba94 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IPositionHistoryRestClient : ISharedClient { /// - /// Position history request options + /// Position history request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetPositionHistoryOptions GetPositionHistoryOptions { get; } /// - /// Get position history + /// Get position history, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs index 733bf8be..2c00cc1e 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -14,25 +15,29 @@ namespace CryptoExchange.Net.SharedApis SharedPositionModeSelection PositionModeSettingType { get; } /// - /// Position mode request options + /// Position mode request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetPositionModeOptions GetPositionModeOptions { get; } /// - /// Get the current position mode setting + /// Get the current position mode setting, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct = default); + Task> GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct = default); /// - /// Position mode set request options + /// Position mode set request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
SetPositionModeOptions SetPositionModeOptions { get; } /// - /// Set the position mode to a new value + /// Set the position mode to a new value, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct = default); + Task> SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs index bea8a315..78e6b5ad 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,27 +10,31 @@ namespace CryptoExchange.Net.SharedApis public interface IAssetsRestClient : ISharedClient { /// - /// Asset request options + /// Asset request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetAssetOptions { get; } + GetAssetOptions GetAssetOptions { get; } /// - /// Get info on a specific asset + /// Get info on a specific asset, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default); + Task> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default); /// - /// Assets request options + /// Assets request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetAssetsOptions { get; } + GetAssetsOptions GetAssetsOptions { get; } /// - /// Get info on all assets the exchange supports + /// Get info on all assets the exchange supports, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default); + Task> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs index adf3e6d7..91ca2236 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IBalanceRestClient : ISharedClient { /// - /// Balances request options + /// Balances request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetBalancesOptions GetBalancesOptions { get; } /// - /// Get balances for the user + /// Get balances for the user, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default); + Task> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs index a0ca57dd..76bcc5cd 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IBookTickerRestClient : ISharedClient { /// - /// Book ticker request options + /// Book ticker request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetBookTickerOptions { get; } + GetBookTickerOptions GetBookTickerOptions { get; } /// - /// Get the best ask/bid info for a symbol + /// Get the best ask/bid info for a symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default); + Task> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs index 9e1af83f..eb2e25d5 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,30 +10,34 @@ namespace CryptoExchange.Net.SharedApis public interface IDepositRestClient : ISharedClient { /// - /// Deposit addresses request options + /// Deposit addresses request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetDepositAddressesOptions { get; } + GetDepositAddressesOptions GetDepositAddressesOptions { get; } /// - /// Get deposit addresses for an asset + /// Get deposit addresses for an asset, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default); + Task> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default); /// - /// Deposits request options + /// Deposits request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetDepositsOptions GetDepositsOptions { get; } /// - /// Get deposit records + /// Get deposit records, see for request options and exchange specific required/optional parameters.
///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs index 9f73ff09..2d60ec54 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using System.Threading; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.SharedApis { @@ -9,15 +10,17 @@ namespace CryptoExchange.Net.SharedApis public interface IFeeRestClient : ISharedClient { /// - /// Fee request options + /// Fee request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetFeeOptions { get; } + GetFeeOptions GetFeeOptions { get; } /// - /// Get trading fees for a symbol + /// Get trading fees for a symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default); + Task> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs index 81e1c515..5be5c945 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,17 +10,20 @@ namespace CryptoExchange.Net.SharedApis public interface IKlineRestClient : ISharedClient { /// - /// Kline request options + /// Kline request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetKlinesOptions GetKlinesOptions { get; } /// - /// Get kline/candlestick data + /// Get kline/candlestick data, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs deleted file mode 100644 index 5ffd6cd6..00000000 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Client for managing the listen key for user stream updates - /// - public interface IListenKeyRestClient : ISharedClient - { - /// - /// Start listen key request options - /// - EndpointOptions StartOptions { get; } - /// - /// Get the listen key which can be used for user data updates on the socket client - /// - /// Request info - /// Cancellation token - /// - Task> StartListenKeyAsync(StartListenKeyRequest request, CancellationToken ct = default); - /// - /// Keep-alive listen key request options - /// - EndpointOptions KeepAliveOptions { get; } - /// - /// Keep-alive the listen key, needs to be called at a regular interval (typically every 30 minutes) - /// - /// Request info - /// Cancellation token - /// - Task> KeepAliveListenKeyAsync(KeepAliveListenKeyRequest request, CancellationToken ct = default); - /// - /// Stop listen key request options - /// - EndpointOptions StopOptions { get; } - /// - /// Stop the listen key, updates will no longer be send to the user data stream for this listen key - /// - /// Request info - /// Cancellation token - /// - Task> StopListenKeyAsync(StopListenKeyRequest request, CancellationToken ct = default); - } -} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs index 2994d587..d4088824 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IOrderBookRestClient : ISharedClient { /// - /// Order book request options + /// Order book request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetOrderBookOptions GetOrderBookOptions { get; } /// - /// Get the order book for a symbol + /// Get the order book for a symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct = default); + Task> GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs index ea1aad88..85cd9c6b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IRecentTradeRestClient : ISharedClient { /// - /// Recent trades request options + /// Recent trades request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetRecentTradesOptions GetRecentTradesOptions { get; } /// - /// Get the most recent public trades + /// Get the most recent public trades, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct = default); + Task> GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs index 327db6e3..2d5ac119 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,17 +10,20 @@ namespace CryptoExchange.Net.SharedApis public interface ITradeHistoryRestClient : ISharedClient { /// - /// Trade history request options + /// Trade history request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
GetTradeHistoryOptions GetTradeHistoryOptions { get; } /// - /// Get public trade history + /// Get public trade history, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs index 32e0f076..dc9aac6a 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITransferRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,15 +10,17 @@ namespace CryptoExchange.Net.SharedApis public interface ITransferRestClient : ISharedClient { /// - /// Transfer request options + /// Transfer request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
TransferOptions TransferOptions { get; } /// - /// Transfer funds between account types + /// Transfer funds between account types, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> TransferAsync(TransferRequest request, CancellationToken ct = default); + Task> TransferAsync(TransferRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs index 4ad2ac09..f315ef45 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis public interface IWithdrawRestClient : ISharedClient { /// - /// Withdraw request options + /// Withdraw request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
WithdrawOptions WithdrawOptions { get; } /// - /// Request a withdrawal + /// Request a withdrawal, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> WithdrawAsync(WithdrawRequest request, CancellationToken ct = default); + Task> WithdrawAsync(WithdrawRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs deleted file mode 100644 index fd21b0b2..00000000 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Client for retrieving withdrawal records - /// - public interface IWithdrawalRestClient : ISharedClient - { - /// - /// Withdrawal record request options - /// - GetWithdrawalsOptions GetWithdrawalsOptions { get; } - - /// - /// Get withdrawal records - /// - /// Request info - /// The pagination request from the previous request result `NextPageRequest` property to continue pagination - /// Cancellation token - /// - Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); - } -} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient.cs new file mode 100644 index 00000000..7a041156 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient.cs @@ -0,0 +1,29 @@ +using CryptoExchange.Net.Objects; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Client for retrieving withdrawal records + /// + public interface IWithdrawalRestClient : ISharedClient + { + /// + /// Withdrawal records request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. + ///
+ GetWithdrawalsOptions GetWithdrawalsOptions { get; } + + /// + /// Get withdrawal records, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. + ///
+ /// Request info + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination + /// Cancellation token + /// + Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs index 438eb446..89e0be33 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using System.Threading; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.SharedApis { @@ -9,26 +10,30 @@ namespace CryptoExchange.Net.SharedApis public interface ISpotOrderClientIdRestClient : ISharedClient { /// - /// Spot get order by client order id request options + /// Spot get order by client order id request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetSpotOrderByClientOrderIdOptions { get; } + GetSpotOrderByClientOrderIdOptions GetSpotOrderByClientOrderIdOptions { get; } /// - /// Get info on a specific spot order using a client order id + /// Get info on a specific spot order using a client order id, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetSpotOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); + Task> GetSpotOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); /// - /// Spot cancel order by client order id request options + /// Spot cancel order by client order id request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelSpotOrderByClientOrderIdOptions { get; } + CancelSpotOrderByClientOrderIdOptions CancelSpotOrderByClientOrderIdOptions { get; } /// - /// Cancel a spot order using client order id + /// Cancel a spot order using client order id, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); + Task> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs index 6a1bd1ba..7f7c0d36 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -16,17 +17,16 @@ namespace CryptoExchange.Net.SharedApis /// How the asset is determined in which the trading fee is paid /// SharedFeeAssetType SpotFeeAssetType { get; } - /// - /// Supported order types + /// Supported order types for spot orders /// SharedOrderType[] SpotSupportedOrderTypes { get; } /// - /// Supported time in force + /// Supported time in force types for placing spot orders /// SharedTimeInForce[] SpotSupportedTimeInForce { get; } /// - /// Quantity types support + /// Supported quantity types for placing spot orders /// SharedQuantitySupport SpotSupportedOrderQuantity { get; } @@ -37,83 +37,101 @@ namespace CryptoExchange.Net.SharedApis string GenerateClientOrderId(); /// - /// Spot place order request options + /// Spot place order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
PlaceSpotOrderOptions PlaceSpotOrderOptions { get; } /// - /// Place a new spot order + /// Place a new spot order, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// Cancellation token - Task> PlaceSpotOrderAsync(PlaceSpotOrderRequest request, CancellationToken ct = default); + Task> PlaceSpotOrderAsync(PlaceSpotOrderRequest request, CancellationToken ct = default); /// - /// Spot get order request options + /// Spot get order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetSpotOrderOptions { get; } + GetSpotOrderOptions GetSpotOrderOptions { get; } /// - /// Get info on a specific spot order + /// Get info on a specific spot order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetSpotOrderAsync(GetOrderRequest request, CancellationToken ct = default); + Task> GetSpotOrderAsync(GetOrderRequest request, CancellationToken ct = default); /// - /// Spot get open orders request options + /// Spot get open orders request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetOpenSpotOrdersOptions { get; } + GetOpenSpotOrdersOptions GetOpenSpotOrdersOptions { get; } /// - /// Get info on a open spot orders + /// Get info on a open spot orders, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); + Task> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); /// - /// Spot get closed orders request options + /// Spot get closed orders request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetClosedOrdersOptions GetClosedSpotOrdersOptions { get; } + GetSpotClosedOrdersOptions GetClosedSpotOrdersOptions { get; } /// - /// Get info on closed spot orders + /// Get info on closed spot orders, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// - /// Spot get order trades request options + /// Spot get order trades request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetSpotOrderTradesOptions { get; } + GetSpotOrderTradesOptions GetSpotOrderTradesOptions { get; } /// - /// Get trades for a specific spot order + /// Get trades for a specific spot order, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// Cancellation token - Task> GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); + Task> GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); /// - /// Spot user trades request options + /// Spot user trades request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetUserTradesOptions GetSpotUserTradesOptions { get; } + GetSpotUserTradesOptions GetSpotUserTradesOptions { get; } /// - /// Get spot user trade records + /// Get spot user trade records, see for request options and exchange specific required/optional parameters.
+ /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. ///
/// Request info /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); + Task> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// - /// Spot cancel order request options + /// Spot cancel order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelSpotOrderOptions { get; } + CancelSpotOrderOptions CancelSpotOrderOptions { get; } /// - /// Cancel a spot order + /// Cancel a spot order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + Task> CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs index 71f80a5d..30d00092 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs @@ -10,33 +10,35 @@ namespace CryptoExchange.Net.SharedApis public interface ISpotSymbolRestClient : ISharedClient { /// - /// Spot symbols request options + /// Spot symbols request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetSpotSymbolsOptions { get; } + GetSpotSymbolsOptions GetSpotSymbolsOptions { get; } /// /// Get all spot symbols for a specific base asset /// /// Asset, for example `ETH` - Task> GetSpotSymbolsForBaseAssetAsync(string baseAsset); + Task> GetSpotSymbolsForBaseAssetAsync(string baseAsset); /// /// Gets whether the client supports a spot symbol /// /// The symbol - Task> SupportsSpotSymbolAsync(SharedSymbol symbol); + Task> SupportsSpotSymbolAsync(SharedSymbol symbol); /// /// Gets whether the client supports a spot symbol /// /// The symbol name - Task> SupportsSpotSymbolAsync(string symbolName); + Task> SupportsSpotSymbolAsync(string symbolName); /// - /// Get info on all available spot symbols on the exchange + /// Get info on all available spot symbols on the exchange, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); + Task> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs index 68f18d6b..8cb30830 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,24 +10,28 @@ namespace CryptoExchange.Net.SharedApis public interface ISpotTickerRestClient : ISharedClient { /// - /// Spot ticker request options + /// Spot ticker request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetTickerOptions GetSpotTickerOptions { get; } + GetSpotTickerOptions GetSpotTickerOptions { get; } /// - /// Get ticker for a specific spot symbol + /// Get ticker for a specific spot symbol, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetSpotTickerAsync(GetTickerRequest request, CancellationToken ct = default); + Task> GetSpotTickerAsync(GetTickerRequest request, CancellationToken ct = default); /// - /// Spot tickers request options + /// Spot tickers request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- GetTickersOptions GetSpotTickersOptions { get; } + GetSpotTickersOptions GetSpotTickersOptions { get; } /// - /// Get tickers for all spot symbols + /// Get tickers for all spot symbols, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct = default); + Task> GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs index 871019cc..4d37d796 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using CryptoExchange.Net.Objects; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.SharedApis @@ -9,38 +10,44 @@ namespace CryptoExchange.Net.SharedApis public interface ISpotTriggerOrderRestClient : ISharedClient { /// - /// Place spot trigger order options + /// Place spot trigger order options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; } /// - /// Place a new trigger order + /// Place a new trigger order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token /// - Task> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default); + Task> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default); /// - /// Get trigger order request options + /// Get trigger order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions GetSpotTriggerOrderOptions { get; } + GetSpotTriggerOrderOptions GetSpotTriggerOrderOptions { get; } /// - /// Get info on a specific trigger order + /// Get info on a specific trigger order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); + Task> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); /// - /// Cancel trigger order request options + /// Cancel trigger order request options.
+ /// Use and to check for required and optional parameters for the request.
+ /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. ///
- EndpointOptions CancelSpotTriggerOrderOptions { get; } + CancelSpotTriggerOrderOptions CancelSpotTriggerOrderOptions { get; } /// - /// Cancel a trigger order + /// Cancel a trigger order, see for request options and exchange specific required/optional parameters.
///
/// Request info /// Cancellation token - Task> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + Task> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs index 26d1529b..c2095859 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Futures orders subscription options /// - EndpointOptions SubscribeFuturesOrderOptions { get; } + SubscribeFuturesOrderOptions SubscribeFuturesOrderOptions { get; } /// /// Subscribe to user futures order updates @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs index 05697e37..74117786 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using System.Threading; using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.SharedApis { @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Position subscription options /// - EndpointOptions SubscribePositionOptions { get; } + SubscribePositionOptions SubscribePositionOptions { get; } /// /// Subscribe to user position updates @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs index 55910796..424b1064 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Balance subscription options /// - EndpointOptions SubscribeBalanceOptions { get; } + SubscribeBalanceOptions SubscribeBalanceOptions { get; } /// /// Subscribe to user balance updates @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs index 46fcf29a..c83670e3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Book ticker subscription options /// - EndpointOptions SubscribeBookTickerOptions { get; } + SubscribeBookTickerOptions SubscribeBookTickerOptions { get; } /// /// Subscribe to book ticker (best ask/bid) updates for a symbol @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs index 1133940e..881be563 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using System.Threading; using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.SharedApis { @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs index 9e039f24..946a65b8 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs index c4419724..daec014c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs index 9221b09f..0f2b848e 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs index 2572fc66..3ecb5e90 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Trade subscription options /// - EndpointOptions SubscribeTradeOptions { get; } + SubscribeTradeOptions SubscribeTradeOptions { get; } /// /// Subscribe to public trade updates for a symbol @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs index 50ccefaf..6d26a370 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// User trade subscription options /// - EndpointOptions SubscribeUserTradeOptions { get; } + SubscribeUserTradeOptions SubscribeUserTradeOptions { get; } /// /// Subscribe to user trade updates @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs index b07f7846..cd821ca3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Spot orders subscription options /// - EndpointOptions SubscribeSpotOrderOptions { get; } + SubscribeSpotOrderOptions SubscribeSpotOrderOptions { get; } /// /// Subscribe to user spot order updates @@ -22,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); + Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs index 8b715d58..8be680ef 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs @@ -1,4 +1,5 @@ -using System; +using CryptoExchange.Net.Objects; +using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; @@ -10,25 +11,47 @@ namespace CryptoExchange.Net.SharedApis /// public class ExchangeParameters { - private readonly List _parameters; - private readonly static List _staticParameters = new List(); + private readonly static Dictionary _staticProcessParameters = new Dictionary(); + private readonly Dictionary _processParameters; /// - /// ctor + /// Create a new ExchangeParameters instance with the provided parameters set /// - /// The parameters to add + /// Exchange parameters public ExchangeParameters(params ExchangeParameter[] parameters) { - _parameters = parameters.ToList(); + _processParameters = new Dictionary(); + + foreach (var parameter in parameters) + AddValue(parameter.Exchange, parameter.Name, parameter.Value); } /// - /// Add a new parameter value + /// Add a process parameter. Process parameters are used to determine the correct logic to execute, but are not necessarily passed to the API.
+ /// To directly add or override parameters which are passed to the API, use AddRawParameter or AddRawValue instead. ///
- /// + /// The exchange parameter to add public void AddValue(ExchangeParameter exchangeParameter) { - _parameters.Add(exchangeParameter); + AddValue(exchangeParameter.Exchange, exchangeParameter.Name, exchangeParameter.Value); + } + + /// + /// Add a process parameter. Process parameters are used to determine the correct logic to execute, but are not necessarily passed to the API.
+ /// To directly add or override parameters which are passed to the API, use AddRawParameter or AddRawValue instead. + ///
+ /// Exchange to apply the parameter for + /// Parameter name + /// Parameter value + public void AddValue(string exchange, string key, object value) + { + if (!_processParameters.TryGetValue(exchange, out var exchangeParameters)) + { + exchangeParameters = new Parameters(ParameterSerializationSettings.Default); + _processParameters[exchange] = exchangeParameters; + } + + exchangeParameters.AddRaw(key, value); } /// @@ -40,8 +63,8 @@ namespace CryptoExchange.Net.SharedApis /// public bool HasValue(string exchange, string name, Type type) { - var val = _parameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); - val ??= _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); + var val = TryGetValue(_processParameters, exchange, name); + val ??= TryGetValue(_staticProcessParameters, exchange, name); if (val == null) return false; @@ -49,7 +72,7 @@ namespace CryptoExchange.Net.SharedApis try { Type t = Nullable.GetUnderlyingType(type) ?? type; - Convert.ChangeType(val.Value, t); + Convert.ChangeType(val, t); return true; } catch @@ -59,7 +82,7 @@ namespace CryptoExchange.Net.SharedApis } /// - /// Check whether a specific parameter is provided in the default parameters or the provided instance + /// Check whether a specific process parameter is provided in the default parameters or the provided instance /// /// The provided exchange parameter in the request /// The exchange name @@ -72,14 +95,14 @@ namespace CryptoExchange.Net.SharedApis if (provided == true) return true; - var val = _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); + var val = TryGetValue(_staticProcessParameters, exchange, name); if (val == null) return false; try { Type t = Nullable.GetUnderlyingType(type) ?? type; - Convert.ChangeType(val.Value, t); + Convert.ChangeType(val, t); return true; } catch @@ -96,17 +119,18 @@ namespace CryptoExchange.Net.SharedApis /// Parameter name public T? GetValue(string exchange, string name) { - var val = _parameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); + var val = TryGetValue(_processParameters, exchange, name); + val ??= TryGetValue(_staticProcessParameters, exchange, name); if (val == null) return default; - if (val.Value is T typeVal) + if (val is T typeVal) return typeVal; try { Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - return (T)Convert.ChangeType(val.Value, t); + return (T)Convert.ChangeType(val, t); } catch { @@ -123,32 +147,28 @@ namespace CryptoExchange.Net.SharedApis /// Parameter name public static T? GetValue(ExchangeParameters? exchangeParameters, string exchange, string name) { - T? value; - if (exchangeParameters == null) - { - var parameter = _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(name, StringComparison.InvariantCulture)); - if (parameter == null) - return default; + if (exchangeParameters != null) { - if (parameter.Value is T val) - return val; - - try - { - Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - return (T)Convert.ChangeType(parameter.Value, t); - } - catch - { - throw new ArgumentException("Incorrect type for parameter, expected " + typeof(T).Name, name); - } - } - else - { - value = exchangeParameters.GetValue(exchange, name); + var provided = exchangeParameters.GetValue(exchange, name); + if (provided != null) + return provided; } - return value; + var val = TryGetValue(_staticProcessParameters, exchange, name); + if (val == null) + return default; + + try + { + var type = typeof(T); + Type t = Nullable.GetUnderlyingType(type) ?? type; + var result = Convert.ChangeType(val, t); + return (T)result; + } + catch + { + throw new ArgumentException("Incorrect type for parameter, expected " + typeof(T).Name, name); + } } /// @@ -159,14 +179,14 @@ namespace CryptoExchange.Net.SharedApis /// Parameter value public static void SetStaticParameter(string exchange, string key, object value) { - var existing = _staticParameters.SingleOrDefault(x => x.Exchange.Equals(exchange, StringComparison.InvariantCulture) && x.Name.Equals(key, StringComparison.InvariantCulture)); - if (existing != null) + if (!_staticProcessParameters.TryGetValue(exchange, out var exchangeParameters)) { - existing.Value = value; - return; - } + exchangeParameters = new Parameters(ParameterSerializationSettings.Default); + _staticProcessParameters[exchange] = exchangeParameters; + } - _staticParameters.Add(new ExchangeParameter(exchange, key, value)); + exchangeParameters.Remove(key); + exchangeParameters.AddRaw(key, value); } /// @@ -174,7 +194,23 @@ namespace CryptoExchange.Net.SharedApis /// public static void ResetStaticParameters() { - _staticParameters.Clear(); + _staticProcessParameters.Clear(); + } + + /// + /// Reset the static parameters, clears all parameters for an exchange exchanges + /// + public static void ResetStaticExchangeParameters(string exchange) + { + _staticProcessParameters.Remove(exchange); + } + + private static object? TryGetValue(Dictionary list, string exchange, string key) + { + if (!list.TryGetValue(exchange, out var exchangeParams)) + return null; + + return exchangeParams.SingleOrDefault(x => x.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).Value; } } -} +} \ No newline at end of file diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs deleted file mode 100644 index 8532966d..00000000 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs +++ /dev/null @@ -1,55 +0,0 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// A CallResult from an exchange - /// - /// - public class ExchangeResult : CallResult - { - /// - /// The exchange - /// - public string Exchange { get; } - - /// - /// ctor - /// - public ExchangeResult( - string exchange, - Error error) : - base(error) - { - Exchange = exchange; - } - - /// - /// ctor - /// - public ExchangeResult( - string exchange, - CallResult result) : - base( - result.Data, - result.OriginalData, - result.Error) - { - Exchange = exchange; - } - - /// - /// ctor - /// - public ExchangeResult( - string exchange, - T result) : - base(result, null, null) - { - Exchange = exchange; - } - - /// - public override string ToString() => $"{Exchange} - " + base.ToString(); - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs deleted file mode 100644 index 6819f79a..00000000 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs +++ /dev/null @@ -1,153 +0,0 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// A WebCallResult from an exchange - /// - /// The result type - public class ExchangeWebResult : WebCallResult - { - /// - /// The exchange - /// - public string Exchange { get; } - - /// - /// The trade modes for which the result data is - /// - public TradingMode[]? DataTradeMode { get; } - - /// - /// Next page request, can be passed to the next request on the same endpoint to get the next page - /// - public PageRequest? NextPageRequest { get; } - - /// - /// ctor - /// - public ExchangeWebResult( - string exchange, - Error error) : - base(error) - { - Exchange = exchange; - } - - /// - /// ctor - /// - public ExchangeWebResult( - string exchange, - TradingMode dataTradeMode, - WebCallResult result, - PageRequest? nextPageToken = null) : - base(result.ResponseStatusCode, - result.HttpVersion, - result.ResponseHeaders, - result.ResponseTime, - result.ResponseLength, - result.OriginalData, - result.RequestId, - result.RequestUrl, - result.RequestBody, - result.RequestMethod, - result.RequestHeaders, - result.DataSource, - result.Data, - result.Error) - { - DataTradeMode = new[] { dataTradeMode }; - Exchange = exchange; - NextPageRequest = nextPageToken; - } - - /// - /// ctor - /// - public ExchangeWebResult( - string exchange, - TradingMode[]? dataTradeModes, - WebCallResult result, - PageRequest? nextPageRequest = null) : - base(result.ResponseStatusCode, - result.HttpVersion, - result.ResponseHeaders, - result.ResponseTime, - result.ResponseLength, - result.OriginalData, - result.RequestId, - result.RequestUrl, - result.RequestBody, - result.RequestMethod, - result.RequestHeaders, - result.DataSource, - result.Data, - result.Error) - { - DataTradeMode = dataTradeModes; - Exchange = exchange; - NextPageRequest = nextPageRequest; - } - - /// - /// Create a new result - /// - public ExchangeWebResult( - string exchange, - TradingMode[]? dataTradeModes, - HttpStatusCode? code, - Version? httpVersion, - HttpResponseHeaders? responseHeaders, - TimeSpan? responseTime, - long? responseLength, - string? originalData, - int? requestId, - string? requestUrl, - string? requestBody, - HttpMethod? requestMethod, - HttpRequestHeaders? requestHeaders, - ResultDataSource dataSource, - [AllowNull] T data, - Error? error, - PageRequest? nextPageToken = null) : base( - code, - httpVersion, - responseHeaders, - responseTime, - responseLength, - originalData, - requestId, - requestUrl, - requestBody, - requestMethod, - requestHeaders, - dataSource, - data, - error) - { - DataTradeMode = dataTradeModes; - Exchange = exchange; - NextPageRequest = nextPageToken; - } - - /// - /// Copy the ExchangeWebResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public new ExchangeWebResult As([AllowNull] K data) - { - return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageRequest); - } - - /// - public override string ToString() => $"{Exchange} - " + base.ToString(); - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderByClientOrderIdOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderByClientOrderIdOptions.cs new file mode 100644 index 00000000..8f5448f6 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderByClientOrderIdOptions.cs @@ -0,0 +1,15 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for canceling a futures order by client order id + /// + public class CancelFuturesOrderByClientOrderIdOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelFuturesOrderByClientOrderIdOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderClientIdRestClient.CancelFuturesOrderByClientOrderIdAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderOptions.cs new file mode 100644 index 00000000..3d27bca8 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for canceling a futures order + /// + public class CancelFuturesOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelFuturesOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderRestClient.CancelFuturesOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTpSlOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTpSlOptions.cs new file mode 100644 index 00000000..50d93daa --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTpSlOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for canceling a TP/SL + /// + public class CancelFuturesTpSlOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelFuturesTpSlOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesTpSlRestClient.CancelFuturesTpSlAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTriggerOrderOptions.cs new file mode 100644 index 00000000..d62079b8 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelFuturesTriggerOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for canceling spot trigger order + /// + public class CancelFuturesTriggerOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelFuturesTriggerOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesTriggerOrderRestClient.CancelFuturesTriggerOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderByClientOrderIdOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderByClientOrderIdOptions.cs new file mode 100644 index 00000000..d0b18aca --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderByClientOrderIdOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for canceling a spot order + /// + public class CancelSpotOrderByClientOrderIdOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelSpotOrderByClientOrderIdOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotOrderClientIdRestClient.CancelSpotOrderByClientOrderIdAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderOptions.cs new file mode 100644 index 00000000..757271d7 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for canceling a spot order + /// + public class CancelSpotOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelSpotOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotOrderRestClient.CancelSpotOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotTriggerOrderOptions.cs new file mode 100644 index 00000000..ae940bea --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/CancelSpotTriggerOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting spot trigger order + /// + public class CancelSpotTriggerOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public CancelSpotTriggerOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotTriggerOrderRestClient.CancelSpotTriggerOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/ClosePositionOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/ClosePositionOptions.cs new file mode 100644 index 00000000..491f41a2 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/ClosePositionOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for closing position + /// + public class ClosePositionOptions : EndpointOptions + { + /// + /// ctor + /// + public ClosePositionOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderRestClient.ClosePositionAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs index 58b8d1d6..6acc0444 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Text; namespace CryptoExchange.Net.SharedApis @@ -10,22 +11,26 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for an exchange endpoint /// - public class EndpointOptions + public abstract class EndpointOptions { /// - /// Required exchange-specific parameters + /// Required exchange-specific parameters. These can be provided via the `exchangeParameters` property of the request object. /// public List RequiredExchangeParameters { get; set; } = new List(); /// - /// Optional exchange-specific parameters + /// Optional exchange-specific parameters. These can be provided via the `exchangeParameters` property of the request object. /// public List OptionalExchangeParameters { get; set; } = new List(); /// + /// Exchange + /// + public string Exchange { get; set; } + /// /// Endpoint name /// public string EndpointName { get; set; } /// - /// Information on the specific exchange request + /// Exchange specific additional info /// public string? RequestNotes { get; set; } /// @@ -33,15 +38,16 @@ namespace CryptoExchange.Net.SharedApis /// public bool NeedsAuthentication { get; set; } /// - /// Whether the call is supported by the exchange + /// Whether the call is supported. If false the exchange API does not support this operation. /// public bool Supported { get; set; } = true; /// /// ctor /// - public EndpointOptions(string endpointName, bool needAuthentication) + public EndpointOptions(string exchange, string endpointName, bool needAuthentication) { + Exchange = exchange; EndpointName = endpointName; NeedsAuthentication = needAuthentication; } @@ -49,62 +55,67 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - /// Exchange name /// Provided exchange parameters /// Request trading mode /// Supported trading modes /// - public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + public virtual Error? ValidateRequest(ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) { + if (!Supported) + return ArgumentError.Invalid("Endpoint", $"Endpoint {Exchange} {EndpointName} 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 (!string.IsNullOrEmpty(param.Name)) - { - if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true) - 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 ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); - } + { + if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, Exchange, x, param.ValueType) != true)) + return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}"); } return null; } - - /// - public virtual string ToString(string exchange) - { - if (!Supported) - return $"{exchange} {EndpointName} NOT SUPPORTED"; - - var sb = new StringBuilder(); - sb.AppendLine($"{exchange} {EndpointName}"); - if (!string.IsNullOrEmpty(RequestNotes)) - sb.AppendLine(RequestNotes); - sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); - sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); - sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", OptionalExchangeParameters.Select(x => x.ToString()))}"); - return sb.ToString(); - } } /// /// Options for an exchange endpoint /// - /// Type of data + /// Type of data #if NET5_0_OR_GREATER - public class EndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest + public class EndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TRequest> : EndpointOptions + where TRequest : SharedRequest #else - public class EndpointOptions : EndpointOptions where T : SharedRequest + public abstract class EndpointOptions : EndpointOptions + where TRequest : SharedRequest #endif { /// - /// Required optional parameters in the request + /// ctor + /// + public EndpointOptions(string exchange, bool needsAuthentication, string requestName) : base(exchange, requestName, needsAuthentication) + { + } + } + + /// + /// Options for an exchange endpoint + /// + /// Type of data + /// Type of the client +#if NET5_0_OR_GREATER + public abstract class EndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TRequest, TClient> : EndpointOptions + where TRequest : SharedRequest + where TClient : ISharedClient +#else + public abstract class EndpointOptions : EndpointOptions + where TRequest : SharedRequest + where TClient : ISharedClient +#endif + { + private static PropertyInfo[] _requestProperties = typeof(TRequest).GetProperties(); + + /// + /// Required optional parameters in the request. These can be provided via the `exchangeParameters` property of the request object. /// public List RequiredOptionalParameters { get; set; } = new List(); @@ -120,33 +131,25 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public EndpointOptions(bool needsAuthentication) : base(typeof(T).Name, needsAuthentication) + public EndpointOptions(string exchange, bool needsAuthentication, string requestName) : base(exchange, needsAuthentication, requestName) { } /// /// Validate a request /// - /// Exchange name /// The request - /// Request trading mode - /// Supported trading modes + /// Containing client /// - public virtual Error? ValidateRequest(string exchange, T request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + public virtual Error? ValidateRequest(TRequest request, TClient client) { + if (NeedsAuthentication && !client.Authenticated) + return new NoApiCredentialsError(); + foreach (var param in RequiredOptionalParameters) { - if (!string.IsNullOrEmpty(param.Name)) - { - if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null) - 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 ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); - } - + if (param.Names!.All(x => _requestProperties.Single(p => p.Name == x).GetValue(request, null) == null)) + 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) @@ -162,26 +165,38 @@ namespace CryptoExchange.Net.SharedApis } - return ValidateRequest(exchange, request.ExchangeParameters, tradingMode, supportedTradingModes); + return ValidateRequest(request.ExchangeParameters, request.TradingMode, client.SupportedTradingModes); } /// - public override string ToString(string exchange) + public override string ToString() { if (!Supported) - return $"{exchange} {EndpointName} NOT SUPPORTED"; + return $"{Exchange} {EndpointName} NOT SUPPORTED"; var sb = new StringBuilder(); - sb.AppendLine($"{exchange} {typeof(T).Name}"); + sb.AppendLine($"{Exchange} {EndpointName}"); sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); if (!string.IsNullOrEmpty(RequestNotes)) sb.AppendLine(RequestNotes); if (RequiredOptionalParameters.Any()) - sb.AppendLine($"Required optional parameters: {string.Join(", ", RequiredOptionalParameters.Select(x => x.ToString()))}"); + { + sb.AppendLine($"Required optional parameters:"); + foreach(var param in RequiredOptionalParameters) + sb.AppendLine($" {param}"); + } if (RequiredExchangeParameters.Any()) - sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + { + sb.AppendLine($"Required exchange specific parameters:"); + foreach (var param in RequiredExchangeParameters) + sb.AppendLine($" {param}"); + } if (OptionalExchangeParameters.Any()) - sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + { + sb.AppendLine($"Optional exchange specific parameters:"); + foreach (var param in OptionalExchangeParameters) + sb.AppendLine($" {param}"); + } return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetOptions.cs new file mode 100644 index 00000000..1c274ede --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting asset info + /// + public class GetAssetOptions : EndpointOptions + { + /// + /// ctor + /// + public GetAssetOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IAssetsRestClient.GetAssetAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetsOptions.cs new file mode 100644 index 00000000..003a7829 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetAssetsOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting assets info + /// + public class GetAssetsOptions : EndpointOptions + { + /// + /// ctor + /// + public GetAssetsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IAssetsRestClient.GetAssetsAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBalancesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBalancesOptions.cs index 1355cbb1..0e7c2e92 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBalancesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBalancesOptions.cs @@ -6,7 +6,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting a transfer /// - public class GetBalancesOptions : EndpointOptions + public class GetBalancesOptions : EndpointOptions { /// /// Supported account types @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetBalancesOptions(params AccountTypeFilter[] accountTypes) : base(true) + public GetBalancesOptions(string exchange, params AccountTypeFilter[] accountTypes) : base(exchange, true, nameof(IBalanceRestClient.GetBalancesAsync)) { SupportedAccountTypes = accountTypes; } @@ -24,15 +24,14 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public Error? ValidateRequest( - string exchange, + public override Error? ValidateRequest( GetBalancesRequest request, - TradingMode[] supportedApiTypes) + IBalanceRestClient client) { if (request.AccountType != null && !IsValid(request.AccountType.Value)) return ArgumentError.Invalid(nameof(request.AccountType), "Invalid AccountType"); - return base.ValidateRequest(exchange, request, null, supportedApiTypes); + return base.ValidateRequest(request, client); } /// diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBookTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBookTickerOptions.cs new file mode 100644 index 00000000..221fb7c6 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetBookTickerOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting book ticker + /// + public class GetBookTickerOptions : EndpointOptions + { + /// + /// ctor + /// + public GetBookTickerOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IBookTickerRestClient.GetBookTickerAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs deleted file mode 100644 index 12f2f942..00000000 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Text; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Options for requesting closed orders - /// - public class GetClosedOrdersOptions : PaginatedEndpointOptions - { - /// - /// ctor - /// - public GetClosedOrdersOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) - { - } - - /// - public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (!SupportsAscending && request.Direction == DataDirection.Ascending) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); - - if (!SupportsDescending && request.Direction == DataDirection.Descending) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); - - if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); - - if (!TimePeriodFilterSupport) - { - // When going descending we can still allow startTime filter to limit the results - var now = DateTime.UtcNow; - if ((request.Direction != DataDirection.Descending && request.StartTime != null) - || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) - { - return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); - } - } - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); - return sb.ToString(); - } - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositAddressesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositAddressesOptions.cs new file mode 100644 index 00000000..12e2ed9a --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositAddressesOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting deposit address + /// + public class GetDepositAddressesOptions : EndpointOptions + { + /// + /// ctor + /// + public GetDepositAddressesOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IDepositRestClient.GetDepositAddressesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs index 88e39052..3cde027e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs @@ -7,18 +7,18 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting deposits /// - public class GetDepositsOptions : PaginatedEndpointOptions + public class GetDepositsOptions : PaginatedEndpointOptions { /// /// ctor /// - public GetDepositsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) + public GetDepositsOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(IDepositRestClient.GetDepositsAsync)) { } /// - public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetDepositsRequest request, IDepositRestClient client) { if (!SupportsAscending && request.Direction == DataDirection.Ascending) return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); @@ -40,15 +40,7 @@ namespace CryptoExchange.Net.SharedApis } } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); - return sb.ToString(); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFeeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFeeOptions.cs new file mode 100644 index 00000000..4f4d1310 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFeeOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting trading fee info + /// + public class GetFeeOptions : EndpointOptions + { + /// + /// ctor + /// + public GetFeeOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFeeRestClient.GetFeesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs index 114b6456..f21c54e9 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs @@ -7,18 +7,18 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting funding rate history /// - public class GetFundingRateHistoryOptions : PaginatedEndpointOptions + public class GetFundingRateHistoryOptions : PaginatedEndpointOptions { /// /// ctor /// - public GetFundingRateHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) + public GetFundingRateHistoryOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IFundingRateRestClient.GetFundingRateHistoryAsync)) { } /// - public override Error? ValidateRequest(string exchange, GetFundingRateHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetFundingRateHistoryRequest request, IFundingRateRestClient client) { if (!SupportsAscending && request.Direction == DataDirection.Ascending) return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); @@ -40,15 +40,7 @@ namespace CryptoExchange.Net.SharedApis } } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); - return sb.ToString(); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesClosedOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesClosedOrdersOptions.cs new file mode 100644 index 00000000..bcba3c84 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesClosedOrdersOptions.cs @@ -0,0 +1,47 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting closed orders + /// + public class GetFuturesClosedOrdersOptions : PaginatedEndpointOptions + { + /// + /// ctor + /// + public GetFuturesClosedOrdersOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(IFuturesOrderRestClient.GetClosedFuturesOrdersAsync)) + { + } + + /// + public override Error? ValidateRequest(GetClosedOrdersRequest request, IFuturesOrderRestClient client) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(request, client); + } + } + +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderByClientOrderIdOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderByClientOrderIdOptions.cs new file mode 100644 index 00000000..f44c0859 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderByClientOrderIdOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a spot order by client order id + /// + public class GetFuturesOrderByClientOrderIdOptions : EndpointOptions + { + /// + /// ctor + /// + public GetFuturesOrderByClientOrderIdOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderClientIdRestClient.GetFuturesOrderByClientOrderIdAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderOptions.cs new file mode 100644 index 00000000..b1a95fc0 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a futures order by id endpoint + /// + public class GetFuturesOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public GetFuturesOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderRestClient.GetFuturesOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderTradesOptions.cs new file mode 100644 index 00000000..34b888e8 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesOrderTradesOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a trades for an order + /// + public class GetFuturesOrderTradesOptions : EndpointOptions + { + /// + /// ctor + /// + public GetFuturesOrderTradesOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderRestClient.GetFuturesOrderTradesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesSymbolsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesSymbolsOptions.cs new file mode 100644 index 00000000..8663b466 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesSymbolsOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting symbol info + /// + public class GetFuturesSymbolsOptions : EndpointOptions + { + /// + /// ctor + /// + public GetFuturesSymbolsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesSymbolRestClient.GetFuturesSymbolsAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickerOptions.cs new file mode 100644 index 00000000..6a1719d1 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickerOptions.cs @@ -0,0 +1,31 @@ +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting ticker + /// + public class GetFuturesTickerOptions : EndpointOptions + { + /// + /// Type of ticker calculation + /// + public SharedTickerType TickerType { get; set; } = SharedTickerType.Day24H; + + /// + /// ctor + /// + public GetFuturesTickerOptions(string exchange, SharedTickerType? tickerCalcType = null) : base(exchange, false, nameof(IFuturesTickerRestClient.GetFuturesTickerAsync)) + { + TickerType = tickerCalcType ?? SharedTickerType.Day24H; + } + + /// + public override string ToString() + { + var sb = new StringBuilder(base.ToString()); + sb.AppendLine($"Ticker data calculation type: {TickerType}"); + return sb.ToString(); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickersOptions.cs new file mode 100644 index 00000000..40b27e8d --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTickersOptions.cs @@ -0,0 +1,31 @@ +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting tickers + /// + public class GetFuturesTickersOptions : EndpointOptions + { + /// + /// Type of ticker calculation + /// + public SharedTickerType TickerType { get; set; } = SharedTickerType.Day24H; + + /// + /// ctor + /// + public GetFuturesTickersOptions(string exchange, SharedTickerType? tickerCalcType = null) : base(exchange, false, nameof(IFuturesTickerRestClient.GetFuturesTickersAsync)) + { + TickerType = tickerCalcType ?? SharedTickerType.Day24H; + } + + /// + public override string ToString() + { + var sb = new StringBuilder(base.ToString()); + sb.AppendLine($"Ticker data calculation type: {TickerType}"); + return sb.ToString(); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTriggerOrderOptions.cs new file mode 100644 index 00000000..50bf6fa1 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesTriggerOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting futures trigger order + /// + public class GetFuturesTriggerOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public GetFuturesTriggerOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesTriggerOrderRestClient.GetFuturesTriggerOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesUserTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesUserTradesOptions.cs new file mode 100644 index 00000000..cbaf2687 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFuturesUserTradesOptions.cs @@ -0,0 +1,46 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting user trades + /// + public class GetFuturesUserTradesOptions : PaginatedEndpointOptions + { + /// + /// ctor + /// + public GetFuturesUserTradesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(IFuturesOrderRestClient.GetFuturesUserTradesAsync)) + { + } + + /// + public override Error? ValidateRequest(GetUserTradesRequest request, IFuturesOrderRestClient client) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetUserTradesRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetUserTradesRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetUserTradesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(request, client); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetIndexPriceKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetIndexPriceKlinesOptions.cs new file mode 100644 index 00000000..cd63497d --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetIndexPriceKlinesOptions.cs @@ -0,0 +1,117 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting kline/candlestick data + /// + public class GetIndexPriceKlinesOptions : PaginatedEndpointOptions + { + /// + /// The supported kline intervals + /// + public SharedKlineInterval[] SupportIntervals { get; } + /// + /// Max number of data points which can be requested + /// + public int? MaxTotalDataPoints { get; set; } + + /// + /// ctor + /// + public GetIndexPriceKlinesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IIndexPriceKlineRestClient.GetIndexPriceKlinesAsync)) + { + SupportIntervals = new[] + { + SharedKlineInterval.OneMinute, + SharedKlineInterval.ThreeMinutes, + SharedKlineInterval.FiveMinutes, + SharedKlineInterval.FifteenMinutes, + SharedKlineInterval.ThirtyMinutes, + SharedKlineInterval.OneHour, + SharedKlineInterval.TwoHours, + SharedKlineInterval.FourHours, + SharedKlineInterval.SixHours, + SharedKlineInterval.EightHours, + SharedKlineInterval.TwelveHours, + SharedKlineInterval.OneDay, + SharedKlineInterval.OneWeek, + SharedKlineInterval.OneMonth + }; + } + + /// + /// ctor + /// + public GetIndexPriceKlinesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IIndexPriceKlineRestClient.GetIndexPriceKlinesAsync)) + { + SupportIntervals = intervals; + } + + /// + /// Check whether a specific interval is supported + /// + /// + /// + public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); + + /// + public override Error? ValidateRequest(GetKlinesRequest request, IIndexPriceKlineRestClient client) + { + if (!IsSupported(request.Interval)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), $"Interval {request.Interval} not supported"); + + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available"); + + if (request.Limit > MaxLimit) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction == DataDirection.Ascending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + if (MaxTotalDataPoints.HasValue) + { + if (request.Limit > MaxTotalDataPoints.Value) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available"); + + if (request.StartTime.HasValue == true) + { + if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); + } + } + + return base.ValidateRequest(request, client); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(base.ToString()); + sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}"); + if (MaxTotalDataPoints != null) + sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); + return sb.ToString(); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs index 3cf3e5d9..fc6f621f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs @@ -8,7 +8,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting kline/candlestick data /// - public class GetKlinesOptions : PaginatedEndpointOptions + public class GetKlinesOptions : PaginatedEndpointOptions { /// /// The supported kline intervals @@ -22,8 +22,8 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) + public GetKlinesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IKlineRestClient.GetKlinesAsync)) { SupportIntervals = new[] { @@ -47,8 +47,8 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) + public GetKlinesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IKlineRestClient.GetKlinesAsync)) { SupportIntervals = intervals; } @@ -61,10 +61,10 @@ namespace CryptoExchange.Net.SharedApis public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); /// - public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetKlinesRequest request, IKlineRestClient client) { if (!IsSupported(request.Interval)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), $"Interval {request.Interval} not supported"); if (!SupportsAscending && request.Direction == DataDirection.Ascending) return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); @@ -101,17 +101,14 @@ namespace CryptoExchange.Net.SharedApis } } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } /// - public override string ToString(string exchange) + public override string ToString() { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); + var sb = new StringBuilder(base.ToString()); sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}"); - if (MaxAge != null) - sb.AppendLine($"Max age of data: {MaxAge}"); if (MaxTotalDataPoints != null) sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); return sb.ToString(); diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetLeverageOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetLeverageOptions.cs new file mode 100644 index 00000000..f8669438 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetLeverageOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting asset info + /// + public class GetLeverageOptions : EndpointOptions + { + /// + /// ctor + /// + public GetLeverageOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ILeverageRestClient.GetLeverageAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetMarkPriceKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetMarkPriceKlinesOptions.cs new file mode 100644 index 00000000..1be01efa --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetMarkPriceKlinesOptions.cs @@ -0,0 +1,117 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting kline/candlestick data + /// + public class GetMarkPriceKlinesOptions : PaginatedEndpointOptions + { + /// + /// The supported kline intervals + /// + public SharedKlineInterval[] SupportIntervals { get; } + /// + /// Max number of data points which can be requested + /// + public int? MaxTotalDataPoints { get; set; } + + /// + /// ctor + /// + public GetMarkPriceKlinesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IMarkPriceKlineRestClient.GetMarkPriceKlinesAsync)) + { + SupportIntervals = new[] + { + SharedKlineInterval.OneMinute, + SharedKlineInterval.ThreeMinutes, + SharedKlineInterval.FiveMinutes, + SharedKlineInterval.FifteenMinutes, + SharedKlineInterval.ThirtyMinutes, + SharedKlineInterval.OneHour, + SharedKlineInterval.TwoHours, + SharedKlineInterval.FourHours, + SharedKlineInterval.SixHours, + SharedKlineInterval.EightHours, + SharedKlineInterval.TwelveHours, + SharedKlineInterval.OneDay, + SharedKlineInterval.OneWeek, + SharedKlineInterval.OneMonth + }; + } + + /// + /// ctor + /// + public GetMarkPriceKlinesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(IMarkPriceKlineRestClient.GetMarkPriceKlinesAsync)) + { + SupportIntervals = intervals; + } + + /// + /// Check whether a specific interval is supported + /// + /// + /// + public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); + + /// + public override Error? ValidateRequest(GetKlinesRequest request, IMarkPriceKlineRestClient client) + { + if (!IsSupported(request.Interval)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), $"Interval {request.Interval} not supported"); + + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available"); + + if (request.Limit > MaxLimit) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction == DataDirection.Ascending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + if (MaxTotalDataPoints.HasValue) + { + if (request.Limit > MaxTotalDataPoints.Value) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available"); + + if (request.StartTime.HasValue == true) + { + if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); + } + } + + return base.ValidateRequest(request, client); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(base.ToString()); + sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}"); + if (MaxTotalDataPoints != null) + sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); + return sb.ToString(); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenFuturesOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenFuturesOrdersOptions.cs new file mode 100644 index 00000000..30d09787 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenFuturesOrdersOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a futures order by id endpoint + /// + public class GetOpenFuturesOrdersOptions : EndpointOptions + { + /// + /// ctor + /// + public GetOpenFuturesOrdersOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderRestClient.GetOpenFuturesOrdersAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenInterestOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenInterestOptions.cs new file mode 100644 index 00000000..688e0d39 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenInterestOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting open interest + /// + public class GetOpenInterestOptions : EndpointOptions + { + /// + /// ctor + /// + public GetOpenInterestOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IOpenInterestRestClient.GetOpenInterestAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenSpotOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenSpotOrdersOptions.cs new file mode 100644 index 00000000..f3fda948 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOpenSpotOrdersOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a spot order by id endpoint + /// + public class GetOpenSpotOrdersOptions : EndpointOptions + { + /// + /// ctor + /// + public GetOpenSpotOrdersOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotOrderRestClient.GetOpenSpotOrdersAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs index 9abcea7c..4fe0af10 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs @@ -8,7 +8,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting order book /// - public class GetOrderBookOptions : EndpointOptions + public class GetOrderBookOptions : EndpointOptions { /// /// Supported order book depths @@ -27,7 +27,8 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetOrderBookOptions(int minLimit, int maxLimit, bool authenticated) : base(authenticated) + public GetOrderBookOptions(string exchange, int minLimit, int maxLimit, bool authenticated) + : base(exchange, authenticated, nameof(IOrderBookRestClient.GetOrderBookAsync)) { MinLimit = minLimit; MaxLimit = maxLimit; @@ -36,33 +37,34 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetOrderBookOptions(int[] supportedLimits, bool authenticated) : base(authenticated) + public GetOrderBookOptions(string exchange, int[] supportedLimits, bool authenticated) + : base(exchange, authenticated, nameof(IOrderBookRestClient.GetOrderBookAsync)) { SupportedLimits = supportedLimits; } /// - public override Error? ValidateRequest(string exchange, GetOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetOrderBookRequest request, IOrderBookRestClient client) { if (request.Limit == null) - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}"); if (MinLimit.HasValue && request.Limit.Value < MinLimit) - return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MaxLimit}"); + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MinLimit}"); if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value)) return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Limit should be one of " + string.Join(", ", SupportedLimits)); - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } /// - public override string ToString(string exchange) + public override string ToString() { - var sb = new StringBuilder(base.ToString(exchange)); + var sb = new StringBuilder(base.ToString()); sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); return sb.ToString(); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs index 59f82f00..88b2902a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs @@ -7,18 +7,18 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting position history /// - public class GetPositionHistoryOptions : PaginatedEndpointOptions + public class GetPositionHistoryOptions : PaginatedEndpointOptions { /// /// ctor /// - public GetPositionHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) + public GetPositionHistoryOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(IPositionHistoryRestClient.GetPositionHistoryAsync)) { } /// - public override Error? ValidateRequest(string exchange, GetPositionHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetPositionHistoryRequest request, IPositionHistoryRestClient client) { if (!SupportsAscending && request.Direction == DataDirection.Ascending) return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); @@ -40,15 +40,7 @@ namespace CryptoExchange.Net.SharedApis } } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); - return sb.ToString(); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs index 7857d2ba..25238991 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs @@ -3,12 +3,12 @@ /// /// Options for requesting current position mode /// - public class GetPositionModeOptions : EndpointOptions + public class GetPositionModeOptions : EndpointOptions { /// /// ctor /// - public GetPositionModeOptions() : base(true) + public GetPositionModeOptions(string exchange) : base(exchange, true, nameof(IPositionModeRestClient.GetPositionModeAsync)) { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionsOptions.cs new file mode 100644 index 00000000..6ede6874 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionsOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting open positions + /// + public class GetPositionsOptions : EndpointOptions + { + /// + /// ctor + /// + public GetPositionsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderRestClient.GetPositionsAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs index ef32f6b6..91ca1e17 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs @@ -6,7 +6,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting recent trades /// - public class GetRecentTradesOptions : EndpointOptions + public class GetRecentTradesOptions : EndpointOptions { /// /// The max number of trades that can be requested @@ -16,15 +16,16 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetRecentTradesOptions(int limit, bool authenticated) : base(authenticated) + public GetRecentTradesOptions(string exchange, int limit, bool authenticated) + : base(exchange, authenticated, nameof(IRecentTradeRestClient.GetRecentTradesAsync)) { MaxLimit = limit; } /// - public override Error? ValidateRequest(string exchange, GetRecentTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetRecentTradesRequest request, IRecentTradeRestClient client) { - var baseError = base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + var baseError = base.ValidateRequest(request, client); if (baseError != null) return baseError; @@ -35,9 +36,9 @@ namespace CryptoExchange.Net.SharedApis } /// - public override string ToString(string exchange) + public override string ToString() { - var sb = new StringBuilder(base.ToString(exchange)); + var sb = new StringBuilder(base.ToString()); sb.AppendLine($"Max data points: {MaxLimit}"); return sb.ToString(); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotClosedOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotClosedOrdersOptions.cs new file mode 100644 index 00000000..61d0f6c9 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotClosedOrdersOptions.cs @@ -0,0 +1,46 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting user trades + /// + public class GetSpotClosedOrdersOptions : PaginatedEndpointOptions + { + /// + /// ctor + /// + public GetSpotClosedOrdersOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(ISpotOrderRestClient.GetClosedSpotOrdersAsync)) + { + } + + /// + public override Error? ValidateRequest(GetClosedOrdersRequest request, ISpotOrderRestClient client) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(request, client); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderByClientOrderIdOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderByClientOrderIdOptions.cs new file mode 100644 index 00000000..a7e21162 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderByClientOrderIdOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a spot order by client order id endpoint + /// + public class GetSpotOrderByClientOrderIdOptions : EndpointOptions + { + /// + /// ctor + /// + public GetSpotOrderByClientOrderIdOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotOrderClientIdRestClient.GetSpotOrderByClientOrderIdAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderOptions.cs new file mode 100644 index 00000000..27f6e466 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting a spot order by id endpoint + /// + public class GetSpotOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public GetSpotOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotOrderRestClient.GetSpotOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderTradesOptions.cs new file mode 100644 index 00000000..95dabb33 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotOrderTradesOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for getting trades for a spot order + /// + public class GetSpotOrderTradesOptions : EndpointOptions + { + /// + /// ctor + /// + public GetSpotOrderTradesOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotOrderRestClient.GetSpotOrderTradesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotSymbolsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotSymbolsOptions.cs new file mode 100644 index 00000000..ac25a78c --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotSymbolsOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting symbol info + /// + public class GetSpotSymbolsOptions : EndpointOptions + { + /// + /// ctor + /// + public GetSpotSymbolsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotSymbolRestClient.GetSpotSymbolsAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTickerOptions.cs similarity index 55% rename from CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs rename to CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTickerOptions.cs index fee07caa..25f6bf02 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickerOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTickerOptions.cs @@ -5,7 +5,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting ticker /// - public class GetTickerOptions : EndpointOptions + public class GetSpotTickerOptions : EndpointOptions { /// /// Type of ticker calculation @@ -15,16 +15,16 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetTickerOptions(SharedTickerType? tickerCalcType = null) : base(false) + public GetSpotTickerOptions(string exchange, SharedTickerType? tickerCalcType = null) : base(exchange, false, nameof(ISpotTickerRestClient.GetSpotTickerAsync)) { TickerType = tickerCalcType ?? SharedTickerType.Day24H; } /// - public override string ToString(string exchange) + public override string ToString() { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Ticker time calc type: {TickerType}"); + var sb = new StringBuilder(base.ToString()); + sb.AppendLine($"Ticker data calculation type: {TickerType}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTickersOptions.cs similarity index 55% rename from CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs rename to CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTickersOptions.cs index 84885a7f..e6734a9e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTickersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTickersOptions.cs @@ -5,7 +5,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting tickers /// - public class GetTickersOptions : EndpointOptions + public class GetSpotTickersOptions : EndpointOptions { /// /// Type of ticker calculation @@ -15,16 +15,16 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetTickersOptions(SharedTickerType? tickerCalcType = null) : base(false) + public GetSpotTickersOptions(string exchange, SharedTickerType? tickerCalcType = null) : base(exchange, false, nameof(ISpotTickerRestClient.GetSpotTickersAsync)) { TickerType = tickerCalcType ?? SharedTickerType.Day24H; } /// - public override string ToString(string exchange) + public override string ToString() { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Ticker time calc type: {TickerType}"); + var sb = new StringBuilder(base.ToString()); + sb.AppendLine($"Ticker data calculation type: {TickerType}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTriggerOrderOptions.cs new file mode 100644 index 00000000..fcfabcb7 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotTriggerOrderOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting spot trigger order + /// + public class GetSpotTriggerOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public GetSpotTriggerOrderOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotTriggerOrderRestClient.GetSpotTriggerOrderAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotUserTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotUserTradesOptions.cs new file mode 100644 index 00000000..a89d956e --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetSpotUserTradesOptions.cs @@ -0,0 +1,46 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting user trades + /// + public class GetSpotUserTradesOptions : PaginatedEndpointOptions + { + /// + /// ctor + /// + public GetSpotUserTradesOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(ISpotOrderRestClient.GetSpotUserTradesAsync)) + { + } + + /// + public override Error? ValidateRequest(GetUserTradesRequest request, ISpotOrderRestClient client) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetUserTradesRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetUserTradesRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetUserTradesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(request, client); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs index 6c174dc7..e64d4cb0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs @@ -7,29 +7,29 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting trade history /// - public class GetTradeHistoryOptions : PaginatedEndpointOptions + public class GetTradeHistoryOptions : PaginatedEndpointOptions { /// /// ctor /// - public GetTradeHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) + public GetTradeHistoryOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication, nameof(ITradeHistoryRestClient.GetTradeHistoryAsync)) { } /// - public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetTradeHistoryRequest request, ITradeHistoryRestClient client) { if (!SupportsAscending && request.Direction == DataDirection.Ascending) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.Direction), $"Ascending direction is not supported"); if (!SupportsDescending && request.Direction == DataDirection.Descending) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.Direction), $"Descending direction is not supported"); if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs deleted file mode 100644 index f59d4480..00000000 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Text; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Options for requesting user trades - /// - public class GetUserTradesOptions : PaginatedEndpointOptions - { - /// - /// ctor - /// - public GetUserTradesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) - { - } - - /// - public override Error? ValidateRequest(string exchange, GetUserTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (!SupportsAscending && request.Direction == DataDirection.Ascending) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); - - if (!SupportsDescending && request.Direction == DataDirection.Descending) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); - - if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); - - if (!TimePeriodFilterSupport) - { - // When going descending we can still allow startTime filter to limit the results - var now = DateTime.UtcNow; - if ((request.Direction != DataDirection.Descending && request.StartTime != null) - || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) - { - return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); - } - } - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); - return sb.ToString(); - } - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs index fa8a2bcd..85841df7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs @@ -7,18 +7,18 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting withdrawals /// - public class GetWithdrawalsOptions : PaginatedEndpointOptions + public class GetWithdrawalsOptions : PaginatedEndpointOptions { /// /// ctor /// - public GetWithdrawalsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) - : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) + public GetWithdrawalsOptions(string exchange, bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(exchange, supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true, nameof(IWithdrawalRestClient.GetWithdrawalsAsync)) { } /// - public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(GetWithdrawalsRequest request, IWithdrawalRestClient client) { if (!SupportsAscending && request.Direction == DataDirection.Ascending) return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); @@ -27,7 +27,7 @@ namespace CryptoExchange.Net.SharedApis return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); if (!TimePeriodFilterSupport) { @@ -36,19 +36,11 @@ namespace CryptoExchange.Net.SharedApis if ((request.Direction != DataDirection.Descending && request.StartTime != null) || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) { - return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported"); } } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); - return sb.ToString(); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs index e61cf11a..1927c915 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs @@ -7,11 +7,14 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for paginated endpoints /// - /// #if NET5_0_OR_GREATER - public class PaginatedEndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest + public class PaginatedEndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TRequest, TClient> : EndpointOptions + where TRequest : SharedRequest + where TClient : ISharedClient #else - public class PaginatedEndpointOptions : EndpointOptions where T : SharedRequest + public abstract class PaginatedEndpointOptions : EndpointOptions + where TRequest : SharedRequest + where TClient : ISharedClient #endif { /// @@ -42,11 +45,13 @@ namespace CryptoExchange.Net.SharedApis /// ctor /// public PaginatedEndpointOptions( + string exchange, bool supportsAscending, bool supportsDescending, bool timePeriodSupport, int maxLimit, - bool needsAuthentication) : base(needsAuthentication) + bool needsAuthentication, + string requestName) : base(exchange, needsAuthentication, requestName) { SupportsAscending = supportsAscending; SupportsDescending = supportsDescending; @@ -55,14 +60,15 @@ namespace CryptoExchange.Net.SharedApis } /// - public override string ToString(string exchange) + public override string ToString() { - var sb = new StringBuilder(base.ToString(exchange)); + var sb = new StringBuilder(base.ToString()); sb.AppendLine($"Ascending retrieval supported: {SupportsAscending}"); sb.AppendLine($"Descending retrieval supported: {SupportsDescending}"); - sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}"); + sb.AppendLine($"Time period filter supported: {TimePeriodFilterSupport}"); sb.AppendLine($"Max limit: {MaxLimit}"); - sb.AppendLine($"Max age: {MaxAge}"); + if (MaxAge.HasValue) + sb.AppendLine($"Max age: {MaxAge}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs index 4683cced..6cc9c15f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs @@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for placing a new futures order /// - public class PlaceFuturesOrderOptions : EndpointOptions + public class PlaceFuturesOrderOptions : EndpointOptions { /// /// Whether or not the API supports setting take profit / stop loss with the order @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public PlaceFuturesOrderOptions(bool supportsTpSl) : base(true) + public PlaceFuturesOrderOptions(string exchange, bool supportsTpSl) : base(exchange, true, nameof(IFuturesOrderRestClient.PlaceFuturesOrderAsync)) { SupportsTpSl = supportsTpSl; } @@ -25,14 +25,10 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public Error? ValidateRequest( - string exchange, + public override Error? ValidateRequest( PlaceFuturesOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedOrderType[] supportedOrderTypes, - SharedTimeInForce[] supportedTimeInForce, - SharedQuantitySupport quantitySupport) + IFuturesOrderRestClient client + ) { if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.StopLossPrice) + " / " + nameof(PlaceFuturesOrderRequest.TakeProfitPrice), "Tp/Sl parameters not supported"); @@ -40,17 +36,17 @@ namespace CryptoExchange.Net.SharedApis if (request.OrderType == SharedOrderType.Other) throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); - if (!supportedOrderTypes.Contains(request.OrderType)) + if (!client.FuturesSupportedOrderTypes.Contains(request.OrderType)) return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.OrderType), "Order type not supported"); - if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) + if (request.TimeInForce != null && !client.FuturesSupportedTimeInForce.Contains(request.TimeInForce.Value)) return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.TimeInForce), "Order time in force not supported"); - var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); + var quantityError = client.FuturesSupportedOrderQuantity.Validate(request.Side, request.OrderType, request.Quantity); if (quantityError != null) return quantityError; - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs index e68f671c..47032789 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs @@ -5,17 +5,17 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for placing a new spot trigger order /// - public class PlaceFuturesTriggerOrderOptions : EndpointOptions + public class PlaceFuturesTriggerOrderOptions : EndpointOptions { /// - /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. + /// When true the API holds the funds until the order is triggered or canceled. When false the funds will only be required when the order is triggered and will fail if the funds are not available at that time. /// public bool HoldsFunds { get; set; } /// /// ctor /// - public PlaceFuturesTriggerOrderOptions(bool holdsFunds) : base(true) + public PlaceFuturesTriggerOrderOptions(string exchange, bool holdsFunds) : base(exchange, true, nameof(IFuturesTriggerOrderRestClient.PlaceFuturesTriggerOrderAsync)) { HoldsFunds = holdsFunds; } @@ -23,19 +23,15 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public Error? ValidateRequest( - string exchange, + public override Error? ValidateRequest( PlaceFuturesTriggerOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedOrderSide side, - SharedQuantitySupport quantitySupport) + IFuturesTriggerOrderRestClient client) { - var quantityError = quantitySupport.Validate(side, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); - if (quantityError != null) - return quantityError; + //var quantityError = client.FuturesSupportedOrderQuantity.Validate(request.OrderDirection, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); + //if (quantityError != null) + // return quantityError; - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs index de6a6f27..8a3ebbbb 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs @@ -7,42 +7,37 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for placing a new spot order /// - public class PlaceSpotOrderOptions : EndpointOptions + public class PlaceSpotOrderOptions : EndpointOptions { /// /// ctor /// - public PlaceSpotOrderOptions() : base(true) + public PlaceSpotOrderOptions(string exchange) : base(exchange, true, nameof(ISpotOrderRestClient.PlaceSpotOrderAsync)) { } /// /// Validate a request /// - public Error? ValidateRequest( - string exchange, + public override Error? ValidateRequest( PlaceSpotOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedOrderType[] supportedOrderTypes, - SharedTimeInForce[] supportedTimeInForce, - SharedQuantitySupport quantitySupport) + ISpotOrderRestClient client) { if (request.OrderType == SharedOrderType.Other) throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); - if (!supportedOrderTypes.Contains(request.OrderType)) + if (!client.SpotSupportedOrderTypes.Contains(request.OrderType)) return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.OrderType), "Order type not supported"); - if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) + if (request.TimeInForce != null && !client.SpotSupportedTimeInForce.Contains(request.TimeInForce.Value)) return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.TimeInForce), "Order time in force not supported"); - var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); + var quantityError = client.SpotSupportedOrderQuantity.Validate(request.Side, request.OrderType, request.Quantity); if (quantityError != null) return quantityError; - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs index b9323a02..bd4c78be 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs @@ -5,17 +5,17 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for placing a new spot trigger order /// - public class PlaceSpotTriggerOrderOptions : EndpointOptions + public class PlaceSpotTriggerOrderOptions : EndpointOptions { /// - /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. + /// When true the API holds the funds until the order is triggered or canceled. When false the funds will only be required when the order is triggered and will fail if the funds are not available at that time. /// public bool HoldsFunds { get; set; } /// /// ctor /// - public PlaceSpotTriggerOrderOptions(bool holdsFunds) : base(true) + public PlaceSpotTriggerOrderOptions(string exchange, bool holdsFunds) : base(exchange, true, nameof(ISpotTriggerOrderRestClient.PlaceSpotTriggerOrderAsync)) { HoldsFunds = holdsFunds; } @@ -23,18 +23,15 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public Error? ValidateRequest( - string exchange, + public override Error? ValidateRequest( PlaceSpotTriggerOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedQuantitySupport quantitySupport) + ISpotOrderRestClient client) { - var quantityError = quantitySupport.Validate(request.OrderSide, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); + var quantityError = client.SpotSupportedOrderQuantity.Validate(request.OrderSide, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); if (quantityError != null) return quantityError; - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetFuturesTpSlOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetFuturesTpSlOptions.cs new file mode 100644 index 00000000..6253609f --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetFuturesTpSlOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for setting a TP/SL + /// + public class SetFuturesTpSlOptions : EndpointOptions + { + /// + /// ctor + /// + public SetFuturesTpSlOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesTpSlRestClient.SetFuturesTpSlAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs index e776588a..de3664d4 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs @@ -3,12 +3,12 @@ /// /// Options for setting leverage /// - public class SetLeverageOptions : EndpointOptions + public class SetLeverageOptions : EndpointOptions { /// /// ctor /// - public SetLeverageOptions() : base(true) + public SetLeverageOptions(string exchange) : base(exchange, true, nameof(ILeverageRestClient.SetLeverageAsync)) { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs index e74913cc..99a7cf89 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs @@ -3,12 +3,12 @@ /// /// Options for setting position mode /// - public class SetPositionModeOptions : EndpointOptions + public class SetPositionModeOptions : EndpointOptions { /// /// ctor /// - public SetPositionModeOptions() : base(true) + public SetPositionModeOptions(string exchange) : base(exchange, true, nameof(IPositionModeRestClient.SetPositionModeOptions)) { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/TransferOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/TransferOptions.cs index c0abf9e3..15021236 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/TransferOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/TransferOptions.cs @@ -6,7 +6,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for requesting a transfer /// - public class TransferOptions : EndpointOptions + public class TransferOptions : EndpointOptions { /// /// Supported account types @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public TransferOptions(SharedAccountType[] accountTypes) : base(true) + public TransferOptions(string exchange, SharedAccountType[] accountTypes) : base(exchange, true, nameof(ITransferRestClient.TransferAsync)) { SupportedAccountTypes = accountTypes; } @@ -24,11 +24,9 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public new Error? ValidateRequest( - string exchange, + public override Error? ValidateRequest( TransferRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes) + ITransferRestClient client) { if (!SupportedAccountTypes.Contains(request.FromAccountType)) return ArgumentError.Invalid(nameof(request.FromAccountType), "Invalid FromAccountType"); @@ -36,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis if (!SupportedAccountTypes.Contains(request.ToAccountType)) return ArgumentError.Invalid(nameof(request.FromAccountType), "Invalid ToAccountType"); - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs index 17dfda8e..17df177d 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs @@ -3,12 +3,12 @@ /// /// Options for requesting a withdrawal /// - public class WithdrawOptions : EndpointOptions + public class WithdrawOptions : EndpointOptions { /// /// ctor /// - public WithdrawOptions() : base(true) + public WithdrawOptions(string exchange) : base(exchange, true, nameof(IWithdrawRestClient.WithdrawAsync)) { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs b/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs index 8a5dc573..317ff86b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace CryptoExchange.Net.SharedApis { @@ -8,13 +9,9 @@ namespace CryptoExchange.Net.SharedApis public class ParameterDescription { /// - /// Name of the parameter + /// Possible names for the parameter. One of these names can be used to provide the parameter in the ExchangeParameters of the request. /// - public string? Name { get; set; } - /// - /// Names of the parameters - /// - public string[]? Names { get; set; } + public string[] Names { get; set; } /// /// Type of the value /// @@ -33,7 +30,7 @@ namespace CryptoExchange.Net.SharedApis /// public ParameterDescription(string parameterName, Type valueType, string description, object exampleValue) { - Name = parameterName; + Names = [parameterName]; ValueType = valueType; Description = description; ExampleValue = exampleValue; @@ -53,9 +50,7 @@ namespace CryptoExchange.Net.SharedApis /// public override string ToString() { - if (Name != null) - return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; - return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}"; + return $"[{ValueType.Name}] {string.Join(" / ", Names.Select(x => $"\"{x}\""))}: {Description}"; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBalanceOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBalanceOptions.cs new file mode 100644 index 00000000..a2a9af8b --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBalanceOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to balance updates + /// + public class SubscribeBalanceOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribeBalanceOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(IBalanceSocketClient.SubscribeToBalanceUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBookTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBookTickerOptions.cs new file mode 100644 index 00000000..0f1f6504 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeBookTickerOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to book ticker updates + /// + public class SubscribeBookTickerOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribeBookTickerOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(IBookTickerSocketClient.SubscribeToBookTickerUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeFuturesOrderOptions.cs new file mode 100644 index 00000000..8ab785cc --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeFuturesOrderOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to order updates + /// + public class SubscribeFuturesOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribeFuturesOrderOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(IFuturesOrderSocketClient.SubscribeToFuturesOrderUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs index ac6f58be..ff5bb90a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs @@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for subscribing to kline/candlestick updates /// - public class SubscribeKlineOptions : EndpointOptions + public class SubscribeKlineOptions : EndpointOptions { /// /// Kline intervals supported for updates @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public SubscribeKlineOptions(bool needsAuthentication) : base(needsAuthentication) + public SubscribeKlineOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(IKlineSocketClient.SubscribeToKlineUpdatesAsync)) { SupportIntervals = new[] { @@ -41,7 +41,8 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public SubscribeKlineOptions(bool needsAuthentication, params SharedKlineInterval[] intervals) : base(needsAuthentication) + public SubscribeKlineOptions(string exchange, bool needsAuthentication, params SharedKlineInterval[] intervals) + : base(exchange, needsAuthentication, nameof(IKlineSocketClient.SubscribeToKlineUpdatesAsync)) { SupportIntervals = intervals; } @@ -56,12 +57,12 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(SubscribeKlineRequest request, IKlineSocketClient client) { if (!IsSupported(request.Interval)) return ArgumentError.Invalid(nameof(SubscribeKlineRequest.Interval), "Interval not supported"); - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs index 8c1abe75..3b979187 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs @@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Options for subscribing to order book snapshot updates /// - public class SubscribeOrderBookOptions : EndpointOptions + public class SubscribeOrderBookOptions : EndpointOptions { /// /// Order book depths supported for updates @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public SubscribeOrderBookOptions(bool needsAuthentication, int[] limits) : base(needsAuthentication) + public SubscribeOrderBookOptions(string exchange, bool needsAuthentication, int[] limits) : base(exchange, needsAuthentication, nameof(IOrderBookSocketClient.SubscribeToOrderBookUpdatesAsync)) { SupportedLimits = limits; } @@ -25,12 +25,12 @@ namespace CryptoExchange.Net.SharedApis /// /// Validate a request /// - public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + public override Error? ValidateRequest(SubscribeOrderBookRequest request, IOrderBookSocketClient client) { if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value)) return ArgumentError.Invalid(nameof(SubscribeOrderBookRequest.Limit), "Limit not supported"); - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + return base.ValidateRequest(request, client); } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribePositionOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribePositionOptions.cs new file mode 100644 index 00000000..95dcf9fc --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribePositionOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to position updates + /// + public class SubscribePositionOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribePositionOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(IPositionSocketClient.SubscribeToPositionUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeSpotOrderOptions.cs new file mode 100644 index 00000000..11de3855 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeSpotOrderOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to order updates + /// + public class SubscribeSpotOrderOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribeSpotOrderOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(ISpotOrderSocketClient.SubscribeToSpotOrderUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs index de629fbd..2d0ce0e8 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickerOptions.cs @@ -3,7 +3,7 @@ /// /// Options for subscribing to ticker updates /// - public class SubscribeTickerOptions : EndpointOptions + public class SubscribeTickerOptions : EndpointOptions { /// /// Type of ticker calculation @@ -13,7 +13,7 @@ /// /// ctor /// - public SubscribeTickerOptions(SharedTickerType? tickerCalcType = null) : base(false) + public SubscribeTickerOptions(string exchange, SharedTickerType? tickerCalcType = null) : base(exchange, false, nameof(ITickerSocketClient.SubscribeToTickerUpdatesAsync)) { TickerType = tickerCalcType ?? SharedTickerType.Day24H; } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs index 3852d9f2..ef2bfd2a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTickersOptions.cs @@ -3,7 +3,7 @@ /// /// Options for subscribing to ticker updates /// - public class SubscribeTickersOptions : EndpointOptions + public class SubscribeTickersOptions : EndpointOptions { /// /// Type of ticker calculation @@ -13,7 +13,7 @@ /// /// ctor /// - public SubscribeTickersOptions(SharedTickerType? tickerCalcType = null) : base(false) + public SubscribeTickersOptions(string exchange, SharedTickerType? tickerCalcType = null) : base(exchange, false, nameof(ITickersSocketClient.SubscribeToAllTickersUpdatesAsync)) { TickerType = tickerCalcType ?? SharedTickerType.Day24H; } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTradeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTradeOptions.cs new file mode 100644 index 00000000..0d78a78f --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeTradeOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to trade updates + /// + public class SubscribeTradeOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribeTradeOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(ITradeSocketClient.SubscribeToTradeUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeUserTradeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeUserTradeOptions.cs new file mode 100644 index 00000000..b8c433f2 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeUserTradeOptions.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for subscribing to user trade updates + /// + public class SubscribeUserTradeOptions : EndpointOptions + { + /// + /// ctor + /// + public SubscribeUserTradeOptions(string exchange, bool needsAuthentication) : base(exchange, needsAuthentication, nameof(IUserTradeSocketClient.SubscribeToUserTradeUpdatesAsync)) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs index c8a0a64a..a1fd01de 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs @@ -15,7 +15,8 @@ /// /// Asset to retrieve info on /// Exchange specific parameters - public GetAssetRequest(string asset, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetAssetRequest(string asset, ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { Asset = asset; } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs index e46f3ec5..e68a006d 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs @@ -9,7 +9,8 @@ /// ctor /// /// Exchange specific parameters - public GetAssetsRequest(ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetAssetsRequest(ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs index 78595a93..8ce9ea21 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs @@ -15,7 +15,8 @@ /// /// Trading mode /// Exchange specific parameters - public GetBalancesRequest(TradingMode tradingMode, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetBalancesRequest(TradingMode tradingMode, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { AccountType = tradingMode.ToAccountType(); } @@ -25,7 +26,8 @@ /// /// Account type /// Exchange specific parameters - public GetBalancesRequest(SharedAccountType? accountType = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetBalancesRequest(SharedAccountType? accountType = null, ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { AccountType = accountType; } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs index 53c060c6..a8723c70 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs @@ -20,7 +20,8 @@ /// Asset name to get address for /// Network name /// Exchange specific parameters - public GetDepositAddressesRequest(string asset, string? network = null, ExchangeParameters? exchangeParameters = null): base(exchangeParameters) + public GetDepositAddressesRequest(string asset, string? network = null, ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { Asset = asset; Network = network; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs index 78fc887f..60996389 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs @@ -37,7 +37,8 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// Data direction /// Exchange specific parameters - public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { Asset = asset; StartTime = startTime; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs index f4062eea..9ed91754 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs @@ -5,10 +5,6 @@ /// public record GetOpenOrdersRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } /// /// Symbol filter /// @@ -19,9 +15,8 @@ /// /// Trading mode /// Exchange specific parameters - public GetOpenOrdersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetOpenOrdersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; } /// @@ -29,7 +24,7 @@ /// /// Symbol to retrieve open orders for /// Exchange specific parameters - public GetOpenOrdersRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetOpenOrdersRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol.TradingMode, exchangeParameters) { Symbol = symbol; } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs index db62b2fa..5754d558 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs @@ -7,10 +7,6 @@ namespace CryptoExchange.Net.SharedApis /// public record GetPositionHistoryRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } /// /// Symbol /// @@ -41,7 +37,8 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// Data direction /// Exchange specific parameters - public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) + : base(symbol.TradingMode, exchangeParameters) { Symbol = symbol; StartTime = startTime; @@ -59,7 +56,8 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// Data direction /// Exchange specific parameters - public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) + : base(tradeMode, exchangeParameters) { TradingMode = tradeMode; StartTime = startTime; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs index 6aaae50d..14c83774 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs @@ -5,10 +5,6 @@ /// public record GetPositionModeRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } /// /// Symbol. Some exchanges set position mode per symbol /// @@ -19,7 +15,8 @@ /// /// Symbol to retrieve position mode for /// Exchange specific parameters - public GetPositionModeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionModeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) + : base(symbol.TradingMode, exchangeParameters) { Symbol = symbol; } @@ -29,9 +26,9 @@ /// /// Trading mode /// Exchange specific parameters - public GetPositionModeRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionModeRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs index 3f6e7c5a..53754b6c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs @@ -5,10 +5,6 @@ /// public record GetPositionsRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } /// /// Symbol filter, required for some exchanges /// @@ -19,9 +15,8 @@ /// /// Trading mode /// Exchange specific parameters - public GetPositionsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; } /// @@ -29,7 +24,7 @@ /// /// Symbol to retriecve positions for /// Exchange specific parameters - public GetPositionsRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionsRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol.TradingMode, exchangeParameters) { Symbol = symbol; } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs index 59fa978f..ce71f088 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs @@ -5,19 +5,13 @@ /// public record GetSymbolsRequest : SharedRequest { - /// - /// Filter by trading mode - /// - public TradingMode? TradingMode { get; set; } - /// /// ctor /// /// Trading mode filter /// Exchange specific parameters - public GetSymbolsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetSymbolsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs index 389e4e1f..af56d2ab 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs @@ -5,19 +5,13 @@ /// public record GetTickersRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// /// ctor /// /// Trading mode /// Exchange specific parameters - public GetTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs index 0438b701..327960ad 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs @@ -37,7 +37,8 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// Data direction /// Exchange specific parameters - public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { Asset = asset; StartTime = startTime; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs deleted file mode 100644 index 459a97fb..00000000 --- a/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Request to keep-alive the update stream for the specified listen key - /// - public record KeepAliveListenKeyRequest : SharedRequest - { - /// - /// The key to stop updates for - /// - public string ListenKey { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - - /// - /// ctor - /// - /// The key to keep alive - /// Trading mode - /// Exchange specific parameters - public KeepAliveListenKeyRequest(string listenKey, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - ListenKey = listenKey; - TradingMode = tradingMode; - } - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs index 42989f76..0b3b3ab6 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs @@ -59,7 +59,6 @@ /// ctor ///
/// Symbol to place the order on - /// Side of the order /// Type of the order /// Quantity of the order diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs index 75b613c2..81d8ba34 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs @@ -10,10 +10,6 @@ ///
public SharedSymbol? Symbol { get; set; } /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// /// Position mode to change to /// public SharedPositionMode PositionMode { get; set; } @@ -24,7 +20,8 @@ /// Position mode to change to /// Trading mode /// Exchange specific parameters - public SetPositionModeRequest(SharedPositionMode positionMode, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SetPositionModeRequest(SharedPositionMode positionMode, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { TradingMode = tradingMode; PositionMode = positionMode; @@ -36,7 +33,8 @@ /// Symbol to change to position mode for /// Position mode to change to /// Exchange specific parameters - public SetPositionModeRequest(SharedSymbol symbol, SharedPositionMode positionMode, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SetPositionModeRequest(SharedSymbol symbol, SharedPositionMode positionMode, ExchangeParameters? exchangeParameters = null) + : base(symbol.TradingMode, exchangeParameters) { PositionMode = positionMode; Symbol = symbol; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs deleted file mode 100644 index 1f6c81b0..00000000 --- a/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Request to start the update stream for the current user - /// - public record StartListenKeyRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public StartListenKeyRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs deleted file mode 100644 index 015cc131..00000000 --- a/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace CryptoExchange.Net.SharedApis -{ - /// - /// Request to stop the update stream for the specific listen key - /// - public record StopListenKeyRequest : SharedRequest - { - /// - /// The key to stop updates for - /// - public string ListenKey { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - - /// - /// ctor - /// - /// The key to stop updates for - /// Trading mode - /// Exchange specific parameters - public StopListenKeyRequest(string listenKey, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - ListenKey = listenKey; - TradingMode = tradingMode; - } - } -} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/TransferRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/TransferRequest.cs index 2b5908b5..fca7bb7e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/TransferRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/TransferRequest.cs @@ -48,7 +48,7 @@ SharedAccountType toAccount, string? fromSymbol = null, string? toSymbol = null, - ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + ExchangeParameters? exchangeParameters = null) : base(null, exchangeParameters) { Asset = asset; Quantity = quantity; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs index d2c8bca3..9b6ffa7e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs @@ -35,7 +35,8 @@ /// Network to use /// Address tag /// Exchange specific parameters - public WithdrawRequest(string asset, decimal quantity, string address, string? network = null, string? addressTag = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public WithdrawRequest(string asset, decimal quantity, string address, string? network = null, string? addressTag = null, ExchangeParameters? exchangeParameters = null) + : base(null, exchangeParameters) { Asset = asset; Address = address; diff --git a/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs b/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs index 2ee2aae8..ff86a144 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs @@ -1,10 +1,17 @@ -namespace CryptoExchange.Net.SharedApis +using System.Linq; + +namespace CryptoExchange.Net.SharedApis { /// /// Request /// public record SharedRequest { + /// + /// Trading mode + /// + public TradingMode? TradingMode { get; set; } + /// /// Exchange parameters. Some calls may require exchange specific parameters to execute the request. /// @@ -13,9 +20,28 @@ /// /// ctor /// - public SharedRequest(ExchangeParameters? exchangeParameters = null) + public SharedRequest(TradingMode? tradingMode, ExchangeParameters? exchangeParameters = null) { + TradingMode = tradingMode; ExchangeParameters = exchangeParameters; } + + /// + /// Get the value of a parameter from this instance or the default values + /// + /// Type of the parameter value + /// Exchange name + /// Parameter name or names + public T? GetParamValue(string exchange, params string[] names) + { + foreach (var name in names) + { + var value = ExchangeParameters.GetValue(ExchangeParameters, exchange, name); + if (value != null) + return value; + } + + return default; + } } } diff --git a/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs b/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs index c2778873..878a4d97 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs @@ -9,10 +9,6 @@ namespace CryptoExchange.Net.SharedApis ///
public record SharedSymbolRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode TradingMode { get; } /// /// The symbol /// @@ -25,25 +21,33 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol.TradingMode, exchangeParameters) { Symbol = symbol; - TradingMode = symbol.TradingMode; } /// /// ctor /// - public SharedSymbolRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SharedSymbolRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) + : base(symbols.FirstOrDefault()?.TradingMode ?? throw new ArgumentException("Empty symbol list"), exchangeParameters) { - if (!symbols.Any()) - throw new ArgumentException("Empty symbol list"); + Symbols = symbols.ToArray(); if (symbols.GroupBy(x => x.TradingMode).Count() > 1) throw new ArgumentException("All symbols in the symbol list should have the same trading mode"); - - Symbols = symbols.ToArray(); - TradingMode = Symbols.First().TradingMode; } + + /// + /// Get the symbol name using the provided formatter + /// + public string SymbolName(Func formatter) + => Symbol?.GetSymbol(formatter) ?? throw new ArgumentException("Symbol is not set"); + + /// + /// Get the symbol names using the provided formatter + /// + public string[] SymbolNames(Func formatter) + => Symbols?.Select(x => x.GetSymbol(formatter)).ToArray() ?? (Symbol != null ? new[] { Symbol.GetSymbol(formatter) } : null) ?? throw new ArgumentException("Symbol is not set"); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs index 8eca717e..d65ca97e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs @@ -5,19 +5,13 @@ ///
public record SubscribeAllTickersRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// /// ctor /// /// Trading mode /// Exchange specific parameters - public SubscribeAllTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SubscribeAllTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs index d9945fb5..5c999eb7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs @@ -5,25 +5,14 @@ ///
public record SubscribeBalancesRequest: SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } - /// /// ctor /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// Trading mode /// Exchange specific parameters - public SubscribeBalancesRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SubscribeBalancesRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; - ListenKey = listenKey; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs index 716b9c91..f154259d 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs @@ -5,25 +5,14 @@ ///
public record SubscribeFuturesOrderRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } - /// /// ctor /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// Trading mode /// Exchange specific parameters - public SubscribeFuturesOrderRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null): base(exchangeParameters) + public SubscribeFuturesOrderRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; - ListenKey = listenKey; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs index 04a6dbb1..ed207e7e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs @@ -18,7 +18,8 @@ namespace CryptoExchange.Net.SharedApis /// The symbol to subscribe to /// Kline interval /// Exchange specific parameters - public SubscribeKlineRequest(SharedSymbol symbol, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public SubscribeKlineRequest(SharedSymbol symbol, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) + : base(symbol, exchangeParameters) { Interval = interval; } @@ -29,7 +30,8 @@ namespace CryptoExchange.Net.SharedApis /// The symbols to subscribe to /// Kline interval /// Exchange specific parameters - public SubscribeKlineRequest(IEnumerable symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + public SubscribeKlineRequest(IEnumerable symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) + : base(symbols, exchangeParameters) { Interval = interval; } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs index 65f744d2..fd5eee92 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs @@ -18,7 +18,8 @@ namespace CryptoExchange.Net.SharedApis /// The symbol to subscribe to /// Order book depth /// Exchange specific parameters - public SubscribeOrderBookRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public SubscribeOrderBookRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) + : base(symbol, exchangeParameters) { Limit = limit; } @@ -28,7 +29,8 @@ namespace CryptoExchange.Net.SharedApis /// /// The symbols to subscribe to /// Exchange specific parameters - public SubscribeOrderBookRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + public SubscribeOrderBookRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) + : base(symbols, exchangeParameters) { } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs index b7c74a5c..8d4bc593 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs @@ -5,25 +5,14 @@ /// public record SubscribePositionRequest: SharedRequest { - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// /// ctor /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// Trading mode /// Exchange specific parameters - public SubscribePositionRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SubscribePositionRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; - ListenKey = listenKey; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs index 3cb88a3d..e73851d8 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs @@ -5,19 +5,13 @@ /// public record SubscribeSpotOrderRequest : SharedRequest { - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } - /// /// ctor /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// Exchange specific parameters - public SubscribeSpotOrderRequest(string? listenKey = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SubscribeSpotOrderRequest(ExchangeParameters? exchangeParameters = null) + : base(SharedApis.TradingMode.Spot, exchangeParameters) { - ListenKey = listenKey; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs index 406bd6f8..fb15db08 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs @@ -12,7 +12,8 @@ namespace CryptoExchange.Net.SharedApis /// /// The symbol to subscribe to /// Exchange specific parameters - public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) + : base(symbol, exchangeParameters) { } @@ -21,7 +22,8 @@ namespace CryptoExchange.Net.SharedApis /// /// The symbols to subscribe to /// Exchange specific parameters - public SubscribeTickerRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + public SubscribeTickerRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) + : base(symbols, exchangeParameters) { } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs index 115cc1f1..d533cd6a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs @@ -12,7 +12,8 @@ namespace CryptoExchange.Net.SharedApis /// /// The symbol to subscribe to /// Exchange specific parameters - public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) + : base(symbol, exchangeParameters) { } @@ -21,7 +22,8 @@ namespace CryptoExchange.Net.SharedApis /// /// The symbols to subscribe to /// Exchange specific parameters - public SubscribeTradeRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + public SubscribeTradeRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) + : base(symbols, exchangeParameters) { } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs index a851f342..55a90f16 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs @@ -5,25 +5,14 @@ /// public record SubscribeUserTradeRequest : SharedRequest { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } - /// /// ctor /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// Trading mode /// Exchange specific parameters - public SubscribeUserTradeRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public SubscribeUserTradeRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) + : base(tradingMode, exchangeParameters) { - TradingMode = tradingMode; - ListenKey = listenKey; } } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs index 23ea5dd8..c3fb93d3 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs @@ -5,6 +5,10 @@ /// public record SharedBalance { + /// + /// Trading modes the balance is for + /// + public TradingMode[] TradingModes { get; set; } /// /// Asset name /// @@ -26,8 +30,15 @@ /// /// ctor /// - public SharedBalance(string asset, decimal available, decimal total) + public SharedBalance(TradingMode tradingMode, string asset, decimal available, decimal total) + : this([tradingMode], asset, available, total) { } + + /// + /// ctor + /// + public SharedBalance(TradingMode[] tradingMode, string asset, decimal available, decimal total) { + TradingModes = tradingMode; Asset = asset; Available = available; Total = total; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs index 190b359f..ec9aad7d 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs @@ -52,16 +52,22 @@ namespace CryptoExchange.Net.SharedApis /// public decimal? Fee { get; set; } + /// + /// Status of the deposit + /// + public SharedTransferStatus Status { get; set; } + /// /// ctor /// - public SharedWithdrawal(string asset, string address, decimal quantity, bool completed, DateTime timestamp) + public SharedWithdrawal(string asset, string address, decimal quantity, bool completed, DateTime timestamp, SharedTransferStatus status) { Asset = asset; Address = address; Quantity = quantity; Completed = completed; Timestamp = timestamp; + Status = status; } } diff --git a/CryptoExchange.Net/SharedApis/SharedClientInfo.cs b/CryptoExchange.Net/SharedApis/SharedClientInfo.cs new file mode 100644 index 00000000..b5ad4047 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/SharedClientInfo.cs @@ -0,0 +1,62 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + + /// + /// Client information + /// + public class SharedClientInfo + { + /// + /// Exchange name + /// + public string Exchange { get; init; } = string.Empty; + /// + /// The client type name + /// + public string TypeName { get; init; } = string.Empty; + /// + /// Environments supported by this client + /// + public string[] SupportedEnvironments { get; set; } = []; + /// + /// Supported trading modes + /// + public TradingMode[] SupportedTradingModes { get; init; } = []; + /// + /// Centralization type of the exchange + /// + public CentralizationType CentralizationType { get; set; } + /// + /// Endpoint/subscription info + /// + public EndpointOptions[] Features { get; init; } = []; + + /// + /// Create a string representation for this client + /// + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"Exchange: {Exchange}"); + sb.AppendLine($"Client: {TypeName}"); + sb.AppendLine($"Supported environments: {string.Join(", ", SupportedEnvironments)}"); + sb.AppendLine($"Supported trading modes: {string.Join(", ", SupportedTradingModes)}"); + sb.AppendLine($"Centralization type: {CentralizationType}"); + sb.AppendLine($"Features:"); + foreach (var feature in Features.Where(x => x.Supported)) + { + sb.AppendLine($" {feature.EndpointName}"); + } + + return sb.ToString(); + } + } + +} diff --git a/CryptoExchange.Net/SharedApis/SharedUtils.cs b/CryptoExchange.Net/SharedApis/SharedUtils.cs new file mode 100644 index 00000000..426c7f09 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/SharedUtils.cs @@ -0,0 +1,180 @@ +using CryptoExchange.Net.Objects; +using System.Collections.Generic; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Shared interfaces utilities + /// + public static class SharedUtils + { + /// + /// Get client information including supported features + /// + public static SharedClientInfo GetClientInfo(PlatformInfo platformInfo, ISharedClient client) + { + return new SharedClientInfo + { + Exchange = client.Exchange, + TypeName = client.GetType().Name, + SupportedEnvironments = platformInfo.SupportedEnvironments, + SupportedTradingModes = client.SupportedTradingModes, + CentralizationType = platformInfo.CentralizationType, + Features = GetAllEndpointOptions(client) + }; + } + + /// + /// Get all supported endpoints for a client + /// + /// + /// + public static EndpointOptions[] GetAllEndpointOptions(ISharedClient client) + { + var clientType = client.GetType(); + var result = new List(); + if (client is IAssetsRestClient assetClient) + { + result.Add(assetClient.GetAssetOptions); + result.Add(assetClient.GetAssetsOptions); + } + if (client is IBalanceRestClient balanceClient) + result.Add(balanceClient.GetBalancesOptions); + if (client is IDepositRestClient depositClient) + { + result.Add(depositClient.GetDepositAddressesOptions); + result.Add(depositClient.GetDepositsOptions); + } + if (client is IKlineRestClient klineClient) + result.Add(klineClient.GetKlinesOptions); + if (client is IOrderBookRestClient orderBookClient) + result.Add(orderBookClient.GetOrderBookOptions); + if (client is IRecentTradeRestClient recentTradeClient) + result.Add(recentTradeClient.GetRecentTradesOptions); + if (client is ITradeHistoryRestClient tradeHistoryClient) + result.Add(tradeHistoryClient.GetTradeHistoryOptions); + if (client is IWithdrawalRestClient withdrawalClient) + result.Add(withdrawalClient.GetWithdrawalsOptions); + if (client is IWithdrawRestClient withdrawClient) + result.Add(withdrawClient.WithdrawOptions); + if (client is IFeeRestClient feeClient) + result.Add(feeClient.GetFeeOptions); + if (client is IBookTickerRestClient bookTickerClient) + result.Add(bookTickerClient.GetBookTickerOptions); + if (client is ITransferRestClient transferClient) + result.Add(transferClient.TransferOptions); + + if (client is ISpotOrderRestClient spotOrderClient) + { + result.Add(spotOrderClient.PlaceSpotOrderOptions); + result.Add(spotOrderClient.CancelSpotOrderOptions); + result.Add(spotOrderClient.GetClosedSpotOrdersOptions); + result.Add(spotOrderClient.GetOpenSpotOrdersOptions); + result.Add(spotOrderClient.GetSpotOrderOptions); + result.Add(spotOrderClient.GetSpotOrderTradesOptions); + result.Add(spotOrderClient.GetSpotUserTradesOptions); + } + if (client is ISpotSymbolRestClient spotSymbolClient) + result.Add(spotSymbolClient.GetSpotSymbolsOptions); + if (client is ISpotTickerRestClient spotTickerClient) + { + result.Add(spotTickerClient.GetSpotTickerOptions); + result.Add(spotTickerClient.GetSpotTickersOptions); + } + if (client is ISpotTriggerOrderRestClient spotTriggerOrderClient) + { + result.Add(spotTriggerOrderClient.CancelSpotTriggerOrderOptions); + result.Add(spotTriggerOrderClient.GetSpotTriggerOrderOptions); + result.Add(spotTriggerOrderClient.PlaceSpotTriggerOrderOptions); + } + if (client is ISpotOrderClientIdRestClient spotOrderClientIdClient) + { + result.Add(spotOrderClientIdClient.CancelSpotOrderByClientOrderIdOptions); + result.Add(spotOrderClientIdClient.GetSpotOrderByClientOrderIdOptions); + } + + if (client is IFundingRateRestClient fundingRateClient) + result.Add(fundingRateClient.GetFundingRateHistoryOptions); + if (client is IFuturesOrderRestClient futuresOrderClient) + { + result.Add(futuresOrderClient.CancelFuturesOrderOptions); + result.Add(futuresOrderClient.ClosePositionOptions); + result.Add(futuresOrderClient.GetClosedFuturesOrdersOptions); + result.Add(futuresOrderClient.GetFuturesOrderOptions); + result.Add(futuresOrderClient.GetFuturesOrderTradesOptions); + result.Add(futuresOrderClient.GetFuturesUserTradesOptions); + result.Add(futuresOrderClient.GetOpenFuturesOrdersOptions); + result.Add(futuresOrderClient.GetPositionsOptions); + result.Add(futuresOrderClient.PlaceFuturesOrderOptions); + } + if (client is IFuturesSymbolRestClient futuresSymbolClient) + result.Add(futuresSymbolClient.GetFuturesSymbolsOptions); + if (client is IFuturesTickerRestClient futuresTickerClient) + { + result.Add(futuresTickerClient.GetFuturesTickerOptions); + result.Add(futuresTickerClient.GetFuturesTickersOptions); + } + if (client is IIndexPriceKlineRestClient indexPriceKlineClient) + result.Add(indexPriceKlineClient.GetIndexPriceKlinesOptions); + if (client is ILeverageRestClient leverageClient) + { + result.Add(leverageClient.GetLeverageOptions); + result.Add(leverageClient.SetLeverageOptions); + } + if (client is IMarkPriceKlineRestClient markPriceKlineClient) + result.Add(markPriceKlineClient.GetMarkPriceKlinesOptions); + if (client is IOpenInterestRestClient openInterestClient) + result.Add(openInterestClient.GetOpenInterestOptions); + if (client is IPositionHistoryRestClient positionHistoryClient) + result.Add(positionHistoryClient.GetPositionHistoryOptions); + if (client is IPositionModeRestClient positionModeClient) + { + result.Add(positionModeClient.SetPositionModeOptions); + result.Add(positionModeClient.GetPositionModeOptions); + } + if (client is IFuturesTpSlRestClient futuresTpSlClient) + { + result.Add(futuresTpSlClient.SetFuturesTpSlOptions); + result.Add(futuresTpSlClient.CancelFuturesTpSlOptions); + } + if (client is IFuturesTriggerOrderRestClient futuresTriggerOrderClient) + { + result.Add(futuresTriggerOrderClient.CancelFuturesTriggerOrderOptions); + result.Add(futuresTriggerOrderClient.GetFuturesTriggerOrderOptions); + result.Add(futuresTriggerOrderClient.PlaceFuturesTriggerOrderOptions); + } + if (client is IFuturesOrderClientIdRestClient futuresOrderClientIdClient) + { + result.Add(futuresOrderClientIdClient.GetFuturesOrderByClientOrderIdOptions); + result.Add(futuresOrderClientIdClient.CancelFuturesOrderByClientOrderIdOptions); + } + + if (client is IBalanceSocketClient balanceSocketClient) + result.Add(balanceSocketClient.SubscribeBalanceOptions); + if (client is IBookTickerSocketClient bookTickerSocketClient) + result.Add(bookTickerSocketClient.SubscribeBookTickerOptions); + if (client is IKlineSocketClient klineSocketClient) + result.Add(klineSocketClient.SubscribeKlineOptions); + if (client is IOrderBookSocketClient orderBookSocketClient) + result.Add(orderBookSocketClient.SubscribeOrderBookOptions); + if (client is ITickerSocketClient tickerSocketClient) + result.Add(tickerSocketClient.SubscribeTickerOptions); + if (client is ITickersSocketClient tickersSocketClient) + result.Add(tickersSocketClient.SubscribeAllTickersOptions); + if (client is ITradeSocketClient tradeSocketClient) + result.Add(tradeSocketClient.SubscribeTradeOptions); + if (client is IUserTradeSocketClient userTradeSocketClient) + result.Add(userTradeSocketClient.SubscribeUserTradeOptions); + + if (client is ISpotOrderSocketClient spotOrderSocketClient) + result.Add(spotOrderSocketClient.SubscribeSpotOrderOptions); + + if (client is IFuturesOrderSocketClient futuresOrderSocketClient) + result.Add(futuresOrderSocketClient.SubscribeFuturesOrderOptions); + if (client is IPositionSocketClient positionSocketClient) + result.Add(positionSocketClient.SubscribePositionOptions); + + return result.ToArray(); + } + } +} diff --git a/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs index cbd90937..cb4399a5 100644 --- a/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs @@ -138,11 +138,11 @@ namespace CryptoExchange.Net.Sockets.Default _sendBuffer = new ConcurrentQueue(); _ctsSource = new CancellationTokenSource(); _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536; - _requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; + _baseAddress = $"{Uri.Scheme}://{Uri.Host}"; + _requestDefinition = new RequestDefinition(_baseAddress, Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; _closeSem = new SemaphoreSlim(1, 1); _socket = CreateSocket(); - _baseAddress = $"{Uri.Scheme}://{Uri.Host}"; } /// @@ -155,7 +155,7 @@ namespace CryptoExchange.Net.Sockets.Default public virtual async Task ConnectAsync(CancellationToken ct) { var connectResult = await ConnectInternalAsync(ct).ConfigureAwait(false); - if (!connectResult) + if (!connectResult.Success) return connectResult; await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); @@ -208,9 +208,9 @@ namespace CryptoExchange.Net.Sockets.Default { if (Parameters.RateLimiter != null) { - var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, _requestDefinition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); - if (!limitResult) - return new CallResult(new ClientRateLimitError("Connection limit reached")); + var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, _requestDefinition, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); + if (!limitResult.Success) + return CallResult.Fail(new ClientRateLimitError("Connection limit reached")); } using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10)); @@ -235,27 +235,36 @@ namespace CryptoExchange.Net.Sockets.Default if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) { await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return new CallResult(new ServerRateLimitError(we.Message, we)); + return CallResult.Fail(new ServerRateLimitError(we.Message, we)); + } + + if (_socket.HttpStatusCode == 0) + { + // No HTTP response, so request probably didn't reach the server. Don't count towards rate limit + if (Parameters.RateLimiter != null) + { + await Parameters.RateLimiter.ResetAsync(RateLimitItemType.Connection, _requestDefinition, null, null, 1, default).ConfigureAwait(false); + } } if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized) - return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); + return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); #else // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // Try to read 429 from the message instead if (we.Message.Contains("429")) { await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return new CallResult(new ServerRateLimitError(we.Message, we)); + return CallResult.Fail(new ServerRateLimitError(we.Message, we)); } #endif } - return new CallResult(new CantConnectError(e)); + return CallResult.Fail(new CantConnectError(e)); } _logger.SocketConnected(Id, Uri); - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -298,7 +307,7 @@ namespace CryptoExchange.Net.Sockets.Default } if (Parameters.RateLimiter != null) - await Parameters.RateLimiter.ResetAsync(RateLimitItemType.Request, _requestDefinition, _baseAddress, null, null, default).ConfigureAwait(false); + await Parameters.RateLimiter.ResetAsync(RateLimitItemType.Request, _requestDefinition, null, null, null, default).ConfigureAwait(false); // Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected var initialDelay = GetReconnectDelay(); @@ -326,7 +335,7 @@ namespace CryptoExchange.Net.Sockets.Default _reconnectAttempt++; var connected = await ConnectInternalAsync(default).ConfigureAwait(false); - if (!connected) + if (!connected.Success) { // Delay between reconnect attempts var delay = GetReconnectDelay(); @@ -523,8 +532,8 @@ namespace CryptoExchange.Net.Sockets.Default { try { - var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, _requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); - if (!limitResult) + var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, _requestDefinition, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); + if (!limitResult.Success) { await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); continue; diff --git a/CryptoExchange.Net/Sockets/Default/Routing/MessageRoute.cs b/CryptoExchange.Net/Sockets/Default/Routing/MessageRoute.cs index 9ed234e8..db31d205 100644 --- a/CryptoExchange.Net/Sockets/Default/Routing/MessageRoute.cs +++ b/CryptoExchange.Net/Sockets/Default/Routing/MessageRoute.cs @@ -36,6 +36,80 @@ namespace CryptoExchange.Net.Sockets.Default.Routing TopicFilter = topicFilter; } + /// + /// Create a void handler + /// + public static MessageRoute CreateVoid(string typeIdentifier) + { + return new EventRoute(typeIdentifier, null, (con, time, originalData, msg) => CallResult.Ok(default!)); + } + + /// + /// Create a router for handling event messages + /// + public static MessageRoute CreateForEvent(string typeIdentifier, Func handler, bool multipleReaders = false) + { + return new EventRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create a router for handling event messages + /// + public static MessageRoute CreateForEvent(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + { + return new EventRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create a router for handling query responses + /// + public static MessageRoute CreateForQuery(string typeIdentifier, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create a router for handling query responses + /// + public static MessageRoute CreateForQuery(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create a router for handling query responses + /// + public static MessageRoute CreateForQuery(string typeIdentifier, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create a router for handling query responses + /// + public static MessageRoute CreateForQuery(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + /// /// Message handler /// @@ -43,9 +117,84 @@ namespace CryptoExchange.Net.Sockets.Default.Routing } /// - /// Message route + /// Query route /// - public class MessageRoute : MessageRoute + public class QueryRoute : MessageRoute + { + private Func?> _handler; + + /// + public override Type DeserializationType { get; } = typeof(TMessage); + + /// + /// ctor + /// + internal QueryRoute(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + : base(typeIdentifier, topicFilter) + { + _handler = handler; + MultipleReaders = multipleReaders; + } + + /// + public override CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data) + { + return _handler(connection, receiveTime, originalData, (TMessage)data); + } + } + + + /// + /// Query route + /// + public class QueryRoute : QueryRoute + { + /// + /// ctor + /// + internal QueryRoute(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + : base(typeIdentifier, topicFilter, handler, multipleReaders) + { + } + + /// + /// Create route without topic filter + /// + public static QueryRoute CreateWithoutTopicFilter(string typeIdentifier, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create route with optional topic filter + /// + public static QueryRoute CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + + /// + /// Create route with topic filter + /// + public static QueryRoute CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func?> handler, bool multipleReaders = false) + { + return new QueryRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }; + } + } + + /// + /// Event message route + /// + public class EventRoute : MessageRoute { private Func _handler; @@ -55,7 +204,7 @@ namespace CryptoExchange.Net.Sockets.Default.Routing /// /// ctor /// - internal MessageRoute(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + internal EventRoute(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) : base(typeIdentifier, topicFilter) { _handler = handler; @@ -64,10 +213,10 @@ namespace CryptoExchange.Net.Sockets.Default.Routing /// /// Create route without topic filter - /// - public static MessageRoute CreateWithoutTopicFilter(string typeIdentifier, Func handler, bool multipleReaders = false) + /// + public static EventRoute CreateWithoutTopicFilter(string typeIdentifier, Func handler, bool multipleReaders = false) { - return new MessageRoute(typeIdentifier, null, handler) + return new EventRoute(typeIdentifier, null, handler) { MultipleReaders = multipleReaders }; @@ -76,9 +225,9 @@ namespace CryptoExchange.Net.Sockets.Default.Routing /// /// Create route with optional topic filter /// - public static MessageRoute CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + public static EventRoute CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) { - return new MessageRoute(typeIdentifier, topicFilter, handler) + return new EventRoute(typeIdentifier, topicFilter, handler) { MultipleReaders = multipleReaders }; @@ -87,9 +236,9 @@ namespace CryptoExchange.Net.Sockets.Default.Routing /// /// Create route with topic filter /// - public static MessageRoute CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func handler, bool multipleReaders = false) + public static EventRoute CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func handler, bool multipleReaders = false) { - return new MessageRoute(typeIdentifier, topicFilter, handler) + return new EventRoute(typeIdentifier, topicFilter, handler) { MultipleReaders = multipleReaders }; diff --git a/CryptoExchange.Net/Sockets/Default/Routing/MessageRouter.cs b/CryptoExchange.Net/Sockets/Default/Routing/MessageRouter.cs index 4a79db6f..01e5ee73 100644 --- a/CryptoExchange.Net/Sockets/Default/Routing/MessageRouter.cs +++ b/CryptoExchange.Net/Sockets/Default/Routing/MessageRouter.cs @@ -54,131 +54,128 @@ namespace CryptoExchange.Net.Sockets.Default.Routing } /// - /// Create message router without specific message handler + /// Create a void handler /// - public static MessageRouter CreateWithoutHandler(string typeIdentifier, bool multipleReaders = false) + public static MessageRouter CreateVoid(string typeIdentifier) { - return new MessageRouter(new MessageRoute(typeIdentifier, null, (con, receiveTime, originalData, msg) => new CallResult(default, null, null), multipleReaders)); + return new MessageRouter(new EventRoute(typeIdentifier, null, (con, time, originalData, msg) => CallResult.Ok(default!))); } /// - /// Create message router without specific message handler + /// Create a router for handling event messages /// - public static MessageRouter CreateWithoutHandler(string typeIdentifier, string topicFilter, bool multipleReaders = false) + public static MessageRouter CreateForEvent(string typeIdentifier, Func handler, bool multipleReaders = false) { - return new MessageRouter(new MessageRoute(typeIdentifier, topicFilter, (con, receiveTime, originalData, msg) => new CallResult(default, null, null), multipleReaders)); - } - - /// - /// Create message router without topic filter - /// - public static MessageRouter CreateWithoutTopicFilter(IEnumerable values, Func handler, bool multipleReaders = false) - { - return new MessageRouter(values.Select(x => new MessageRoute(x, null, handler, multipleReaders)).ToArray()); - } - - /// - /// Create message router without topic filter - /// - public static MessageRouter CreateWithoutTopicFilter(string typeIdentifier, Func handler, bool multipleReaders = false) - { - return new MessageRouter(new MessageRoute(typeIdentifier, null, handler, multipleReaders)); - } - - /// - /// Create message router with topic filter - /// - public static MessageRouter CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func handler, bool multipleReaders = false) - { - return new MessageRouter(new MessageRoute(typeIdentifier, topicFilter, handler, multipleReaders)); - } - - /// - /// Create message router with topic filter - /// - public static MessageRouter CreateWithTopicFilter(IEnumerable typeIdentifiers, string topicFilter, Func handler, bool multipleReaders = false) - { - var routes = new List(); - foreach (var type in typeIdentifiers) - routes.Add(new MessageRoute(type, topicFilter, handler, multipleReaders)); - - return new MessageRouter(routes.ToArray()); - } - - /// - /// Create message router with topic filter - /// - public static MessageRouter CreateWithTopicFilters(string typeIdentifier, IEnumerable topicFilters, Func handler, bool multipleReaders = false) - { - var routes = new List(); - foreach (var filter in topicFilters) - routes.Add(new MessageRoute(typeIdentifier, filter, handler, multipleReaders)); - - return new MessageRouter(routes.ToArray()); - } - - /// - /// Create message router with topic filter - /// - public static MessageRouter CreateWithTopicFilters(IEnumerable typeIdentifiers, IEnumerable topicFilters, Func handler, bool multipleReaders = false) - { - var routes = new List(); - foreach (var type in typeIdentifiers) + return new MessageRouter(new EventRoute(typeIdentifier, null, handler) { - foreach (var filter in topicFilters) - routes.Add(new MessageRoute(type, filter, handler, multipleReaders)); - } - - return new MessageRouter(routes.ToArray()); + MultipleReaders = multipleReaders + }); } /// - /// Create message router with optional topic filter + /// Create a router for handling event messages /// - public static MessageRouter CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + public static MessageRouter CreateForEvent(IEnumerable typeIdentifier, Func handler, bool multipleReaders = false) { - return new MessageRouter(new MessageRoute(typeIdentifier, topicFilter, handler, multipleReaders)); + return new MessageRouter(typeIdentifier.Select(x => new EventRoute(x, null, handler) + { + MultipleReaders = multipleReaders + }).ToArray()); } /// - /// Create message router with optional topic filter + /// Create a router for handling event messages /// - public static MessageRouter CreateWithOptionalTopicFilters(string typeIdentifier, IEnumerable? topicFilters, Func handler, bool multipleReaders = false) + public static MessageRouter CreateForEvent(IEnumerable typeIdentifier, IEnumerable topicFilters, Func handler, bool multipleReaders = false) { - var routes = new List(); - if (topicFilters?.Count() > 0) - { - foreach (var filter in topicFilters) - routes.Add(new MessageRoute(typeIdentifier, filter, handler, multipleReaders)); - } - else - { - routes.Add(new MessageRoute(typeIdentifier, null, handler, multipleReaders)); - } - - return new MessageRouter(routes.ToArray()); - } - - /// - /// Create message router with optional topic filter - /// - public static MessageRouter CreateWithOptionalTopicFilters(IEnumerable typeIdentifiers, IEnumerable? topicFilters, Func handler, bool multipleReaders = false) - { - var routes = new List(); - foreach (var typeIdentifier in typeIdentifiers) - { - if (topicFilters?.Count() > 0) + return new MessageRouter(typeIdentifier.SelectMany(x => { + var routes = new List(); + foreach (var topicFilter in topicFilters) { - foreach (var filter in topicFilters) - routes.Add(new MessageRoute(typeIdentifier, filter, handler, multipleReaders)); + routes.Add(new EventRoute(x, topicFilter, handler) + { + MultipleReaders = multipleReaders + }); } - else - { - routes.Add(new MessageRoute(typeIdentifier, null, handler, multipleReaders)); - } - } + return routes; + }).ToArray()); + } - return new MessageRouter(routes.ToArray()); + /// + /// Create a router for handling event messages + /// + public static MessageRouter CreateForEvent(string typeIdentifier, string? topicFilter, Func handler, bool multipleReaders = false) + { + return new MessageRouter(new EventRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }); + } + + /// + /// Create a router for handling event messages + /// + public static MessageRouter CreateForEvent(string typeIdentifier, IEnumerable topicFilters, Func handler, bool multipleReaders = false) + { + return new MessageRouter(topicFilters.Select(x => new EventRoute(typeIdentifier, x, handler) + { + MultipleReaders = multipleReaders + }).ToArray()); + } + + /// + /// Create a router for handling query responses + /// + public static MessageRouter CreateForQuery(IEnumerable typeIdentifier, Func?> handler, bool multipleReaders = false) + { + return new MessageRouter(typeIdentifier.Select(x => new QueryRoute(x, null, handler) + { + MultipleReaders = multipleReaders + }).ToArray()); + } + + /// + /// Create a router for handling query responses + /// + public static MessageRouter CreateForQuery(string typeIdentifier, Func?> handler, bool multipleReaders = false) + { + return new MessageRouter(new QueryRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }); + } + + /// + /// Create a router for handling query responses + /// + public static MessageRouter CreateForQuery(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + { + return new MessageRouter(new QueryRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }); + } + + /// + /// Create a router for handling query responses + /// + public static MessageRouter CreateForQuery(string typeIdentifier, Func?> handler, bool multipleReaders = false) + { + return new MessageRouter(new QueryRoute(typeIdentifier, null, handler) + { + MultipleReaders = multipleReaders + }); + } + + /// + /// Create a router for handling query responses + /// + public static MessageRouter CreateForQuery(string typeIdentifier, string? topicFilter, Func?> handler, bool multipleReaders = false) + { + return new MessageRouter(new QueryRoute(typeIdentifier, topicFilter, handler) + { + MultipleReaders = multipleReaders + }); } /// diff --git a/CryptoExchange.Net/Sockets/Default/Routing/SubscriptionRouter.cs b/CryptoExchange.Net/Sockets/Default/Routing/SubscriptionRouter.cs index 613315cd..22848009 100644 --- a/CryptoExchange.Net/Sockets/Default/Routing/SubscriptionRouter.cs +++ b/CryptoExchange.Net/Sockets/Default/Routing/SubscriptionRouter.cs @@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Sockets.Default.Routing public override bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result) { - result = CallResult.SuccessResult; + result = CallResult.Ok(); // Routes without topic filter handle both when the message topic is empty and when it is not, so we always call them var handled = false; diff --git a/CryptoExchange.Net/Sockets/Default/SocketConnection.cs b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs index f9865cde..1353fc75 100644 --- a/CryptoExchange.Net/Sockets/Default/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Net.WebSockets; using System.Text; @@ -162,6 +163,21 @@ namespace CryptoExchange.Net.Sockets.Default /// public double IncomingKbps => _socket.IncomingKbps; + /// + /// The connection URI as string + /// + public string ConnectionUriString + { + get + { + if (_connectionUriString != null) + return _connectionUriString; + + _connectionUriString = ConnectionUri.OriginalString.TrimEnd('/'); + return _connectionUriString; + } + } + /// /// The connection uri /// @@ -185,7 +201,7 @@ namespace CryptoExchange.Net.Sockets.Default /// /// Tag for identification /// - public string Tag { get; set; } + public string? Tag { get; set; } /// /// Additional properties for this connection @@ -273,6 +289,7 @@ namespace CryptoExchange.Net.Sockets.Default private ISocketMessageHandler? _byteMessageConverter; private ISocketMessageHandler? _textMessageConverter; + private string? _connectionUriString; private long _lastSequenceNumber; /// @@ -293,11 +310,10 @@ namespace CryptoExchange.Net.Sockets.Default /// /// New socket connection /// - public SocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, string tag) + public SocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient) { _logger = logger; ApiClient = apiClient; - Tag = tag; Properties = new Dictionary(); _socket = socketFactory.CreateWebsocket(logger, this, parameters); @@ -385,7 +401,9 @@ namespace CryptoExchange.Net.Sockets.Default /// protected virtual async Task GetReconnectionUrlAsync() { - return await ApiClient.GetReconnectUriAsync(this).ConfigureAwait(false); + var result = await ApiClient.GetReconnectUriAsync(this).ConfigureAwait(false); + _connectionUriString = null; // Could be changed, reset cached string + return result; } /// @@ -407,7 +425,7 @@ namespace CryptoExchange.Net.Sockets.Default try { var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false); - if (!reconnectSuccessful) + if (!reconnectSuccessful.Success) { _logger.FailedReconnectProcessing(SocketId, reconnectSuccessful.Error!.ToString()); _ = Task.Run(() => ResubscribingFailed?.Invoke(reconnectSuccessful.Error)); @@ -638,6 +656,9 @@ namespace CryptoExchange.Net.Sockets.Default { if (subscription.CancellationTokenRegistration.HasValue) subscription.CancellationTokenRegistration.Value.Dispose(); + + subscription.Status = SubscriptionStatus.Closed; + subscription.TokenExpired -= HandleTokenExpired; } await _socket.CloseAsync().ConfigureAwait(false); @@ -656,6 +677,7 @@ namespace CryptoExchange.Net.Sockets.Default await Task.Delay(50).ConfigureAwait(false); subscription.Status = SubscriptionStatus.Closing; + subscription.TokenExpired -= HandleTokenExpired; if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) { @@ -727,6 +749,11 @@ namespace CryptoExchange.Net.Sockets.Default return true; } + private void HandleTokenExpired(Subscription arg1, TokenManagement.TokenInfo arg2) + { + _ = TriggerReconnectAsync(); + } + /// /// Get a subscription on this connection by id /// @@ -760,10 +787,18 @@ namespace CryptoExchange.Net.Sockets.Default /// Query to send /// Cancellation token /// - public virtual async Task SendAndWaitQueryAsync(Query query, CancellationToken ct = default) + public virtual async Task SendAndWaitQueryAsync(Query query, CancellationToken ct = default) { + var sw = Stopwatch.StartNew(); await SendAndWaitIntAsync(query, ct).ConfigureAwait(false); - return query.Result ?? new CallResult(new TimeoutError()); + sw.Stop(); + if (!query.Completed) + return WebSocketResult.Fail(ApiClient.Exchange, SocketId, sw.Elapsed, query.Id, ConnectionUriString, new TimeoutError()); + + if (!query.Success) + return WebSocketResult.Fail(ApiClient.Exchange, SocketId, sw.Elapsed, query.Id, ConnectionUriString, query.Error); + + return WebSocketResult.Ok(ApiClient.Exchange, SocketId, sw.Elapsed, query.Id, ConnectionUriString); } /// @@ -773,17 +808,25 @@ namespace CryptoExchange.Net.Sockets.Default /// Query to send /// Cancellation token /// - public virtual async Task> SendAndWaitQueryAsync(Query query, CancellationToken ct = default) + public virtual async Task> SendAndWaitQueryAsync(Query query, CancellationToken ct = default) { + var sw = Stopwatch.StartNew(); await SendAndWaitIntAsync(query, ct).ConfigureAwait(false); - return query.TypedResult ?? new CallResult(new TimeoutError()); + sw.Stop(); + if (!query.Completed) + return QueryResult.Fail(ApiClient.Exchange, SocketId, sw.Elapsed, query.Id, query.RequestBody, ConnectionUriString, query.OriginalData, new TimeoutError()); + + if (!query.Success) + return QueryResult.Fail(ApiClient.Exchange, SocketId, sw.Elapsed, query.Id, query.RequestBody, ConnectionUriString, query.OriginalData, query.Error); + + return QueryResult.Ok(ApiClient.Exchange, SocketId, sw.Elapsed, query.Id, query.RequestBody, ConnectionUriString, query.OriginalData, query.Result.Data!); } private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default) { AddMessageProcessor(query); - var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false); - if (!sendResult) + var sendResult = await SendAsync(query).ConfigureAwait(false); + if (!sendResult.Success) { query.Fail(sendResult.Error!); RemoveMessageProcessor(query); @@ -824,22 +867,50 @@ namespace CryptoExchange.Net.Sockets.Default /// /// Send data over the websocket connection /// - /// The type of the object to send - /// The request id - /// The object to send - /// The weight of the message - public virtual ValueTask SendAsync(int requestId, T obj, int weight) + /// The query + public virtual ValueTask SendAsync(Query query) { + if (_serializer is IByteMessageSerializer byteSerializer) { - return SendBytesAsync(requestId, byteSerializer.Serialize(obj), weight); + return SendBytesAsync(query.Id, byteSerializer.Serialize(query.Request), query.Weight); } else if (_serializer is IStringMessageSerializer stringSerializer) { - if (obj is string str) - return SendStringAsync(requestId, str, weight); + if (query.Request is string str) + { + query.RequestBody = str; + return SendStringAsync(query.Id, str, query.Weight); + } - str = stringSerializer.Serialize(obj); + str = stringSerializer.Serialize(query.Request); + query.RequestBody = str; + return SendStringAsync(query.Id, str, query.Weight); + } + + throw new Exception("Unknown serializer when sending message"); + } + + /// + /// Send data over the websocket connection + /// + /// The data to send + /// The weight of the message + /// The id of the request + public virtual ValueTask SendAsync(int requestId, T request, int weight = 1) + { + if (_serializer is IByteMessageSerializer byteSerializer) + { + return SendBytesAsync(requestId, byteSerializer.Serialize(request), weight); + } + else if (_serializer is IStringMessageSerializer stringSerializer) + { + if (request is string str) + { + return SendStringAsync(requestId, str, weight); + } + + str = stringSerializer.Serialize(request); return SendStringAsync(requestId, str, weight); } @@ -858,26 +929,26 @@ namespace CryptoExchange.Net.Sockets.Default { var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); - return new CallResult(new InvalidOperationError(info)); + return CallResult.Fail(new InvalidOperationError(info)); } if (!_socket.IsOpen) { _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); - return new CallResult(new WebError("Failed to send message, socket no longer open")); + return CallResult.Fail(new WebError("Failed to send message, socket no longer open")); } _logger.SendingByteData(SocketId, requestId, data.Length); try { if (!_socket.Send(requestId, data, weight)) - return new CallResult(new WebError("Failed to send message, connection not open")); + return CallResult.Fail(new WebError("Failed to send message, connection not open")); - return CallResult.SuccessResult; + return CallResult.Ok(); } catch (Exception ex) { - return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + return CallResult.Fail(new WebError("Failed to send message: " + ex.Message, exception: ex)); } } @@ -893,33 +964,33 @@ namespace CryptoExchange.Net.Sockets.Default { var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); - return new CallResult(new InvalidOperationError(info)); + return CallResult.Fail(new InvalidOperationError(info)); } if (!_socket.IsOpen) { _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); - return new CallResult(new WebError("Failed to send message, socket no longer open")); + return CallResult.Fail(new WebError("Failed to send message, socket no longer open")); } _logger.SendingData(SocketId, requestId, data); try { if (!_socket.Send(requestId, data, weight)) - return new CallResult(new WebError("Failed to send message, connection not open")); + return CallResult.Fail(new WebError("Failed to send message, connection not open")); - return CallResult.SuccessResult; + return CallResult.Ok(); } catch (Exception ex) { - return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + return CallResult.Fail(new WebError("Failed to send message: " + ex.Message, exception: ex)); } } private async Task ProcessReconnectAsync() { if (!_socket.IsOpen) - return new CallResult(new WebError("Socket not connected")); + return CallResult.Fail(new WebError("Socket not connected")); if (!DedicatedRequestConnection.IsDedicatedRequestConnection) { @@ -929,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets.Default // No need to resubscribe anything _logger.NothingToResubscribeCloseConnection(SocketId); _ = _socket.CloseAsync(); - return CallResult.SuccessResult; + return CallResult.Ok(); } } @@ -939,7 +1010,7 @@ namespace CryptoExchange.Net.Sockets.Default { // If we reconnected a authenticated connection we need to re-authenticate var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false); - if (!authResult) + if (!authResult.Success) { _logger.FailedAuthenticationDisconnectAndRecoonect(SocketId); return authResult; @@ -955,13 +1026,13 @@ namespace CryptoExchange.Net.Sockets.Default while (true) { if (!_socket.IsOpen) - return new CallResult(new WebError("Socket not connected")); + return CallResult.Fail(new WebError("Socket not connected")); var subList = _listeners.OfType().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList(); if (subList.Count == 0) break; - var taskList = new List>(); + var taskList = new List>(); foreach (var subscription in subList) { var subscribeTask = TrySubscribeAsync(subscription, false, default); @@ -970,16 +1041,19 @@ namespace CryptoExchange.Net.Sockets.Default await Task.WhenAll(taskList).ConfigureAwait(false); if (taskList.Any(t => !t.Result.Success)) - return taskList.First(t => !t.Result.Success).Result; + { + var errorResult = taskList.First(t => !t.Result.Success).Result; + return CallResult.Fail(errorResult.Error!); + } batch++; } if (!_socket.IsOpen) - return new CallResult(new WebError("Socket not connected")); + return CallResult.Fail(new WebError("Socket not connected")); _logger.AllSubscriptionResubscribed(SocketId); - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -988,7 +1062,7 @@ namespace CryptoExchange.Net.Sockets.Default /// The subscription /// Whether this is a new subscription, or an existing subscription (resubscribing on reconnected socket) /// Cancellation token - protected internal async Task TrySubscribeAsync(Subscription subscription, bool newSubscription, CancellationToken subCancelToken) + protected internal async Task TrySubscribeAsync(Subscription subscription, bool newSubscription, CancellationToken subCancelToken) { subscription.ConnectionInvocations = 0; @@ -996,14 +1070,14 @@ namespace CryptoExchange.Net.Sockets.Default { if (!subscription.Active) // Can be closed during resubscribing - return CallResult.SuccessResult; + return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, 0, ConnectionUriString); var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); - if (!result) + if (!result.Success) { - _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); + _logger.FailedRequestRevitalization(SocketId, result.Error.ToString()); subscription.Status = SubscriptionStatus.Pending; - return result; + return WebSocketResult.Fail(ApiClient.Exchange, SocketId, default, 0, ConnectionUriString, result.Error); } } @@ -1013,32 +1087,36 @@ namespace CryptoExchange.Net.Sockets.Default { // No sub query, so successful subscription.Status = SubscriptionStatus.Subscribed; - return CallResult.SuccessResult; + return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, 0, ConnectionUriString); } var subCompleteHandler = () => { - subscription.Status = subQuery.Result!.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending; + subscription.Status = subQuery.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending; subscription.HandleSubQueryResponse(this, subQuery.Response); - if (newSubscription && subQuery.Result.Success && subCancelToken != default) + if (newSubscription && subQuery.Success) { - subscription.CancellationTokenRegistration = subCancelToken.Register(async () => + subscription.TokenExpired += HandleTokenExpired; + if (subCancelToken != default) { - _logger.CancellationTokenSetClosingSubscription(SocketId, subscription.Id); - await CloseAsync(subscription).ConfigureAwait(false); - }, false); + subscription.CancellationTokenRegistration = subCancelToken.Register(async () => + { + _logger.CancellationTokenSetClosingSubscription(SocketId, subscription.Id); + await CloseAsync(subscription).ConfigureAwait(false); + }, false); + } } }; subQuery.OnComplete = subCompleteHandler; var subQueryResult = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false); - if (!subQueryResult) + if (!subQueryResult.Success) { _logger.FailedToSubscribe(SocketId, subQueryResult.Error?.ToString()); // If this was a server process error or timeout we still send an unsubscribe to prevent messages coming in later if (newSubscription) await CloseAsync(subscription).ConfigureAwait(false); - return new CallResult(subQueryResult.Error!); + return WebSocketResult.Fail(ApiClient.Exchange, subQueryResult.Error!); } if (!subQuery.ExpectsResponse) @@ -1061,15 +1139,18 @@ namespace CryptoExchange.Net.Sockets.Default internal async Task ResubscribeAsync(Subscription subscription) { if (!_socket.IsOpen) - return new CallResult(new WebError("Socket is not connected")); + return CallResult.Fail(new WebError("Socket is not connected")); var subQuery = subscription.CreateSubscriptionQuery(this); if (subQuery == null) - return CallResult.SuccessResult; + return CallResult.Ok(); var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false); subscription.HandleSubQueryResponse(this, subQuery.Response); - return result; + if (!result.Success) + return CallResult.Fail(result.Error); + + return CallResult.Ok(); } /// @@ -1097,7 +1178,7 @@ namespace CryptoExchange.Net.Sockets.Default /// How often /// Method returning the query to send /// The callback for processing the response - public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { if (queryDelegate == null) throw new ArgumentNullException(nameof(queryDelegate)); diff --git a/CryptoExchange.Net/Sockets/Default/Subscription.cs b/CryptoExchange.Net/Sockets/Default/Subscription.cs index 9ddc18dc..7c025ea3 100644 --- a/CryptoExchange.Net/Sockets/Default/Subscription.cs +++ b/CryptoExchange.Net/Sockets/Default/Subscription.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets.Default.Routing; using CryptoExchange.Net.Sockets.Interfaces; +using CryptoExchange.Net.TokenManagement; using Microsoft.Extensions.Logging; using System; using System.Threading; @@ -46,6 +47,9 @@ namespace CryptoExchange.Net.Sockets.Default if (_status == value) return; + if (value == SubscriptionStatus.Closed) + _ = TokenLease?.ReleaseAsync(); + _status = value; StatusChanged?.Invoke(value); } @@ -71,6 +75,20 @@ namespace CryptoExchange.Net.Sockets.Default /// public bool Authenticated { get; } + private TokenLease? _tokenLease; + /// + /// Current user auth token lease + /// + public TokenLease? TokenLease + { + get => _tokenLease; + set + { + _tokenLease?.Token.Expired -= HandleTokenExpired; + _tokenLease = value; + _tokenLease?.Token.Expired += HandleTokenExpired; + } + } private MessageRouter _router; /// @@ -124,6 +142,11 @@ namespace CryptoExchange.Net.Sockets.Default /// public event Action? OnMessageRouterUpdated; + /// + /// User token expired + /// + public event Action? TokenExpired; + /// /// ctor /// @@ -150,6 +173,12 @@ namespace CryptoExchange.Net.Sockets.Default return query; } + + private void HandleTokenExpired(TokenInfo info) + { + TokenExpired?.Invoke(this, info); + } + /// /// Get the subscribe query to send when subscribing /// diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs index 3d76ac2e..f21c0aee 100644 --- a/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnection.cs @@ -26,9 +26,8 @@ namespace CryptoExchange.Net.Sockets.HighPerf IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, - JsonSerializerOptions serializerOptions, - string tag) - : base(logger, socketFactory, parameters, apiClient, tag) + JsonSerializerOptions serializerOptions) + : base(logger, socketFactory, parameters, apiClient) { _jsonOptions = serializerOptions; } diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs index df53e5ce..94f35642 100644 --- a/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfJsonSocketConnectionFactory.cs @@ -22,9 +22,9 @@ namespace CryptoExchange.Net.Sockets.HighPerf /// public HighPerfSocketConnection CreateHighPerfConnection( - ILogger logger, IWebsocketFactory factory, WebSocketParameters parameters, SocketApiClient client, string address) + ILogger logger, IWebsocketFactory factory, WebSocketParameters parameters, SocketApiClient client) { - return new HighPerfJsonSocketConnection(logger, factory, parameters, client, _options, address); + return new HighPerfJsonSocketConnection(logger, factory, parameters, client, _options); } } } diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs index af1644da..51c1824e 100644 --- a/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfSocketConnection.cs @@ -67,7 +67,7 @@ namespace CryptoExchange.Net.Sockets.HighPerf /// /// Tag for identification /// - public string Tag { get; set; } + public string? Tag { get; set; } /// /// Additional properties for this connection @@ -128,12 +128,11 @@ namespace CryptoExchange.Net.Sockets.HighPerf /// /// New socket connection /// - public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, string tag) + public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient) { _logger = logger; _pipe = new Pipe(); ApiClient = apiClient; - Tag = tag; Properties = new Dictionary(); _socket = socketFactory.CreateHighPerfWebsocket(logger, parameters, _pipe.Writer); @@ -269,25 +268,25 @@ namespace CryptoExchange.Net.Sockets.HighPerf { var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; _logger.LogWarning("[Sckt {SocketId}] {Info}", SocketId, info); - return new CallResult(new InvalidOperationError(info)); + return CallResult.Fail(new InvalidOperationError(info)); } if (!_socket.IsOpen) { _logger.LogWarning("[Sckt {SocketId}] Request failed to send, socket no longer open", SocketId); - return new CallResult(new WebError("Failed to send message, socket no longer open")); + return CallResult.Fail(new WebError("Failed to send message, socket no longer open")); } try { if (!await _socket.SendAsync(data).ConfigureAwait(false)) - return new CallResult(new WebError("Failed to send message, connection not open")); + return CallResult.Fail(new WebError("Failed to send message, connection not open")); - return CallResult.SuccessResult; + return CallResult.Ok(); } catch (Exception ex) { - return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + return CallResult.Fail(new WebError("Failed to send message: " + ex.Message, exception: ex)); } } @@ -301,25 +300,25 @@ namespace CryptoExchange.Net.Sockets.HighPerf { var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; _logger.LogWarning("[Sckt {SocketId}] {Info}", SocketId, info); - return new CallResult(new InvalidOperationError(info)); + return CallResult.Fail(new InvalidOperationError(info)); } if (!_socket.IsOpen) { _logger.LogWarning("[Sckt {SocketId}] Request failed to send, socket no longer open", SocketId); - return new CallResult(new WebError("Failed to send message, socket no longer open")); + return CallResult.Fail(new WebError("Failed to send message, socket no longer open")); } try { if (!await _socket.SendAsync(data).ConfigureAwait(false)) - return new CallResult(new WebError("Failed to send message, connection not open")); + return CallResult.Fail(new WebError("Failed to send message, connection not open")); - return CallResult.SuccessResult; + return CallResult.Ok(); } catch (Exception ex) { - return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + return CallResult.Fail(new WebError("Failed to send message: " + ex.Message, exception: ex)); } } @@ -404,8 +403,8 @@ namespace CryptoExchange.Net.Sockets.HighPerf /// /// ctor /// - public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, string tag) - : base(logger, socketFactory, parameters, apiClient, tag) + public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient) + : base(logger, socketFactory, parameters, apiClient) { _typedSubscriptions = new List>(); } diff --git a/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs b/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs index e2a7907a..9b645b05 100644 --- a/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/HighPerf/HighPerfWebSocketClient.cs @@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Sockets.HighPerf public virtual async Task ConnectAsync(CancellationToken ct) { var connectResult = await ConnectInternalAsync(ct).ConfigureAwait(false); - if (!connectResult) + if (!connectResult.Success) return connectResult; await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); @@ -158,23 +158,23 @@ namespace CryptoExchange.Net.Sockets.HighPerf { #if (NET6_0_OR_GREATER) if (_socket!.HttpStatusCode == HttpStatusCode.TooManyRequests) - return new CallResult(new ServerRateLimitError(we.Message, we)); + return CallResult.Fail(new ServerRateLimitError(we.Message, we)); if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized) - return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); + return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); #else // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // Try to read 429 from the message instead if (we.Message.Contains("429")) - return new CallResult(new ServerRateLimitError(we.Message, we)); + return CallResult.Fail(new ServerRateLimitError(we.Message, we)); #endif } - return new CallResult(new CantConnectError(e)); + return CallResult.Fail(new CantConnectError(e)); } _logger.SocketConnected(Id, Uri); - return CallResult.SuccessResult; + return CallResult.Ok(); } /// diff --git a/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs b/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs index 607574a7..22761b4a 100644 --- a/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs +++ b/CryptoExchange.Net/Sockets/HighPerf/Interfaces/IHighPerfConnectionFactory.cs @@ -15,6 +15,6 @@ namespace CryptoExchange.Net.Sockets.HighPerf.Interfaces /// Create a new websocket connection /// HighPerfSocketConnection CreateHighPerfConnection( - ILogger logger, IWebsocketFactory factory, WebSocketParameters parameters, SocketApiClient client, string address); + ILogger logger, IWebsocketFactory factory, WebSocketParameters parameters, SocketApiClient client); } } diff --git a/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs b/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs index 4291be90..8efaadcf 100644 --- a/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs +++ b/CryptoExchange.Net/Sockets/Interfaces/ISocketConnection.cs @@ -38,7 +38,7 @@ namespace CryptoExchange.Net.Sockets.Interfaces /// /// Tag /// - string Tag { get; set; } + string? Tag { get; set; } /// /// Closed event /// diff --git a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs index 57b5027a..749e2772 100644 --- a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs +++ b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs @@ -25,6 +25,6 @@ namespace CryptoExchange.Net.Sockets /// /// Callback after query /// - public Action? Callback { get; set; } + public Action? Callback { get; set; } } } diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 60fb6359..8719c20d 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -4,6 +4,7 @@ using CryptoExchange.Net.Sockets.Default; using CryptoExchange.Net.Sockets.Default.Routing; using CryptoExchange.Net.Sockets.Interfaces; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -24,6 +25,26 @@ namespace CryptoExchange.Net.Sockets /// public bool Completed { get; set; } + /// + /// Whether this query completed successfully + /// + [MemberNotNullWhen(false, nameof(Error))] + public abstract bool Success { get; } + + /// + /// Error result for this query + /// + public abstract Error? Error { get; } + + /// + /// The original data returned by the query, only available when `OutputOriginalData` is set to `true` in the client options + /// + public abstract string? OriginalData { get; } + /// + /// The request body content + /// + public string? RequestBody { get; set; } + /// /// Timeout for the request /// @@ -50,11 +71,6 @@ namespace CryptoExchange.Net.Sockets /// public DateTime RequestTimestamp { get; set; } - /// - /// Result - /// - public CallResult? Result { get; set; } - /// /// Response /// @@ -141,7 +157,6 @@ namespace CryptoExchange.Net.Sockets } else { - Result = CallResult.SuccessResult; Completed = true; _event.Set(); } @@ -180,9 +195,18 @@ namespace CryptoExchange.Net.Sockets public abstract class Query : Query { /// - /// The typed call result + /// Result /// - public CallResult? TypedResult => (CallResult?)Result; + public CallResult? Result { get; set; } + + /// + [MemberNotNullWhen(false, nameof(Error))] + [MemberNotNullWhen(true, nameof(Result))] + public override bool Success => Result?.Success == true; + /// + public override Error? Error => Result?.Error; + /// + public override string? OriginalData => Result?.OriginalData; /// /// ctor @@ -213,7 +237,7 @@ namespace CryptoExchange.Net.Sockets { // If an error result is already set don't override that MessageRouter.Handle(typeIdentifier, topicFilter, connection, receiveTime, originalData, message, out var result); - Result = result; + Result = (CallResult?)result; handled = Result != null; if (!handled) // Null from Handle means it wasn't actually for this query @@ -237,9 +261,9 @@ namespace CryptoExchange.Net.Sockets return; if (TimeoutBehavior == TimeoutBehavior.Fail) - Result = new CallResult(new TimeoutError()); + Result = CallResult.Fail(new TimeoutError()); else - Result = new CallResult(default, null, default); + Result = CallResult.Ok(default!); Completed = true; _event.Set(); @@ -252,7 +276,7 @@ namespace CryptoExchange.Net.Sockets if (Completed) return; - Result = new CallResult(error); + Result = CallResult.Fail(error); Completed = true; _event.Set(); diff --git a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs index 77cef363..4fc71644 100644 --- a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs +++ b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs @@ -395,9 +395,9 @@ namespace CryptoExchange.Net.Testing.Comparers } else if (objectValue is bool bl) { - if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True" && stringValue != "yes" && stringValue != "YES")) + if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True" && stringValue != "yes" && stringValue != "YES" && stringValue != "enabled")) throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); - if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False" && stringValue != "no" && stringValue != "NO")) + if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False" && stringValue != "no" && stringValue != "NO" && stringValue != "disabled")) throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); } else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs index d8fa836c..78f23e7f 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs @@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Testing.Implementations public Task ConnectAsync(CancellationToken ct) { Connected = CanConnect; - return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError())); + return Task.FromResult(CanConnect ? CallResult.Ok() : CallResult.Fail(new CantConnectError())); } public bool Send(int requestId, string data, int weight) diff --git a/CryptoExchange.Net/Testing/RestIntegrationTest.cs b/CryptoExchange.Net/Testing/RestIntegrationTest.cs index 27811249..5d09c702 100644 --- a/CryptoExchange.Net/Testing/RestIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/RestIntegrationTest.cs @@ -66,7 +66,7 @@ namespace CryptoExchange.Net.Testing /// Properties to ignore when checking for missing fields /// Whether to use the single array item as compare when checking for missing fields public async Task RunAndCheckResult( - Expression>>> expression, + Expression>>> expression, bool authRequest, bool checkMissingFields = false, string? compareNestedProperty = null, @@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Testing var listener = new EnumValueTraceListener(); Trace.Listeners.Add(listener); - WebCallResult result; + HttpResult result; try { result = await expression.Compile().Invoke(client).ConfigureAwait(false); @@ -144,7 +144,7 @@ namespace CryptoExchange.Net.Testing }; var result = await book.StartAsync().ConfigureAwait(false); - if (!result) + if (!result.Success) throw new Exception($"Book failed to start: " + result.Error); await Task.Delay(5000).ConfigureAwait(false); diff --git a/CryptoExchange.Net/Testing/RestRequestValidator.cs b/CryptoExchange.Net/Testing/RestRequestValidator.cs index 8d71bd95..36f0a879 100644 --- a/CryptoExchange.Net/Testing/RestRequestValidator.cs +++ b/CryptoExchange.Net/Testing/RestRequestValidator.cs @@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Testing public class RestRequestValidator where TClient : BaseRestClient { private readonly TClient _client; - private readonly Func _isAuthenticated; + private readonly Func _isAuthenticated; private readonly string _folder; private readonly string _baseAddress; private readonly string? _nestedPropertyForCompare; @@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Testing /// The base address that is expected /// Func for checking if the request is authenticated /// Property to use for compare - public RestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) + public RestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) { _client = client; _folder = folder; @@ -63,7 +63,7 @@ namespace CryptoExchange.Net.Testing /// /// public Task ValidateAsync( - Func>> methodInvoke, + Func>> methodInvoke, string name, string? nestedJsonProperty = null, List? ignoreProperties = null, @@ -87,7 +87,7 @@ namespace CryptoExchange.Net.Testing /// /// public async Task ValidateAsync( - Func>> methodInvoke, + Func>> methodInvoke, string name, string? nestedJsonProperty = null, List? ignoreProperties = null, @@ -144,8 +144,8 @@ namespace CryptoExchange.Net.Testing // Check request/response properties if (result.Error != null) throw new Exception(name + " returned error " + result.Error); - if (_isAuthenticated(result.AsDataless()) != expectedAuth) - throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}"); + if (_isAuthenticated(result) != expectedAuth) + throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}"); if (result.RequestMethod != new HttpMethod(expectedMethod!)) throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) @@ -217,7 +217,7 @@ namespace CryptoExchange.Net.Testing /// /// public async Task ValidateAsync( - Func> methodInvoke, + Func> methodInvoke, string name, List? ignoreParamValidation = null ) @@ -278,7 +278,7 @@ namespace CryptoExchange.Net.Testing throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); - + if (expectedUriParams != null) { // Validate request parameters diff --git a/CryptoExchange.Net/Testing/SharedRestRequestValidator.cs b/CryptoExchange.Net/Testing/SharedRestRequestValidator.cs index 571f8a68..2a41b009 100644 --- a/CryptoExchange.Net/Testing/SharedRestRequestValidator.cs +++ b/CryptoExchange.Net/Testing/SharedRestRequestValidator.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.Testing public class SharedRestRequestValidator where TClient : BaseRestClient { private readonly TClient _client; - private readonly Func _isAuthenticated; + private readonly Func _isAuthenticated; private readonly string _folder; private readonly string _baseAddress; private readonly string? _nestedPropertyForCompare; @@ -32,7 +32,7 @@ namespace CryptoExchange.Net.Testing /// The base address that is expected /// Func for checking if the request is authenticated /// Property to use for compare - public SharedRestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) + public SharedRestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) { _client = client; _folder = folder; @@ -52,7 +52,7 @@ namespace CryptoExchange.Net.Testing /// /// public Task ValidateAsync( - Func>> methodInvoke, + Func>> methodInvoke, string name, EndpointOptions endpointOptions, params Func[] validation) @@ -70,7 +70,7 @@ namespace CryptoExchange.Net.Testing /// /// public async Task ValidateAsync( - Func>> methodInvoke, + Func>> methodInvoke, string name, EndpointOptions endpointOptions, params Func[] validation) where TActualResponse : TResponse @@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Testing if (result.Error != null) throw new Exception(name + " returned error " + result.Error); if (endpointOptions.NeedsAuthentication != expectedAuth) - throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}"); + throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}"); if (result.RequestMethod != new HttpMethod(expectedMethod!)) throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) diff --git a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs index 1501c452..26a7508c 100644 --- a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs @@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Testing /// The call expression /// Whether an update is expected /// Whether this is an authenticated request - public async Task RunAndCheckUpdate(Expression>, Task>>> expression, bool expectUpdate, bool authRequest) + public async Task RunAndCheckUpdate(Expression>, Task>>> expression, bool expectUpdate, bool authRequest) { if (!ShouldRun()) return; @@ -86,7 +86,7 @@ namespace CryptoExchange.Net.Testing evnt.Set(); }; - CallResult result; + WebSocketResult result; try { result = await expression.Compile().Invoke(client, updateHandler).ConfigureAwait(false); diff --git a/CryptoExchange.Net/Testing/SocketRequestValidator.cs b/CryptoExchange.Net/Testing/SocketRequestValidator.cs index 8787d03e..363913a7 100644 --- a/CryptoExchange.Net/Testing/SocketRequestValidator.cs +++ b/CryptoExchange.Net/Testing/SocketRequestValidator.cs @@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Testing } /// - /// Validate a subscription + /// Validate a query /// /// Expected response type /// Client to test @@ -49,7 +49,7 @@ namespace CryptoExchange.Net.Testing /// public async Task ValidateAsync( TClient client, - Func>> methodInvoke, + Func>> methodInvoke, string name, Func? responseMapper = null, string? nestedJsonProperty = null, @@ -172,7 +172,7 @@ namespace CryptoExchange.Net.Testing await task.ConfigureAwait(false); object? result = task.Result.Data; if (responseMapper != null) - result = responseMapper(task.Result.Data); + result = responseMapper(task.Result.Data!); if (!skipResponseValidation) SystemTextJsonComparer.CompareData(name, result, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); diff --git a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs index 322e7436..53680d44 100644 --- a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs +++ b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs @@ -48,8 +48,8 @@ namespace CryptoExchange.Net.Testing /// Subscription delegate 2 /// Name public async Task ValidateConcurrentAsync( - Func>, Task>> methodInvoke1, - Func>, Task>> methodInvoke2, + Func>, Task>> methodInvoke1, + Func>, Task>> methodInvoke2, string name) { var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; @@ -87,8 +87,8 @@ namespace CryptoExchange.Net.Testing int updates1 = 0; int updates2 = 0; - Task> task1; - Task> task2; + Task> task1; + Task> task2; // Invoke subscription method try { @@ -164,9 +164,9 @@ namespace CryptoExchange.Net.Testing } var res = await Task.WhenAll(task1, task2).ConfigureAwait(false); - if (!res[0]) + if (!res[0].Success) throw new Exception("Subscribe failed: " + res[0].Error!.ToString()); - if (!res[1]) + if (!res[1].Success) throw new Exception("Subscribe failed: " + res[1].Error!.ToString()); if (updates1 != 1 || updates2 != 1) @@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Testing /// /// public async Task ValidateAsync( - Func>, Task>> methodInvoke, + Func>, Task>> methodInvoke, string name, string? nestedJsonProperty = null, List? ignoreProperties = null, @@ -228,7 +228,7 @@ namespace CryptoExchange.Net.Testing }; TUpdate? update = default; - Task> task; + Task> task; // Invoke subscription method try { @@ -331,7 +331,7 @@ namespace CryptoExchange.Net.Testing } var res = await task.ConfigureAwait(false); - if (!res) + if (!res.Success) throw new Exception("Subscribe failed: " + res.Error!.ToString()); await _client.UnsubscribeAllAsync().ConfigureAwait(false); diff --git a/CryptoExchange.Net/Testing/TestHelpers.cs b/CryptoExchange.Net/Testing/TestHelpers.cs index 3733566f..1eba0e58 100644 --- a/CryptoExchange.Net/Testing/TestHelpers.cs +++ b/CryptoExchange.Net/Testing/TestHelpers.cs @@ -11,6 +11,7 @@ using System.Text; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Testing.Implementations; @@ -97,18 +98,6 @@ namespace CryptoExchange.Net.Testing /// /// Check a signature matches the expected signature /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// public static void CheckSignature( RestApiClient client, AuthenticationProvider authProvider, @@ -116,34 +105,22 @@ namespace CryptoExchange.Net.Testing string path, Func?, IDictionary?, IDictionary?, string> getSignature, string expectedSignature, - Dictionary? parameters = null, + Parameters? parameters = null, DateTime? time = null, - bool disableOrdering = false, bool compareCase = true, string host = "https://test.test-api.com") { - parameters ??= new Dictionary - { - { "test", 123 }, - { "test2", "abc" } - }; - - if (disableOrdering) - client.OrderParameters = false; - - var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null; - var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null; + var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? parameters : null; + var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? parameters : null; var requestDefinition = new RestRequestConfiguration( - new RequestDefinition(path, method) + new RequestDefinition(host, path, method) { Authenticated = true }, - host, - uriParams ?? new Dictionary(), - bodyParams ?? new Dictionary(), + uriParams, + bodyParams, new Dictionary(), - client.ArraySerialization, client.ParameterPositions[method], client.RequestBodyFormat ); diff --git a/CryptoExchange.Net/TokenManagement/TokenInfo.cs b/CryptoExchange.Net/TokenManagement/TokenInfo.cs new file mode 100644 index 00000000..a84e63ee --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenInfo.cs @@ -0,0 +1,104 @@ +using System; + +namespace CryptoExchange.Net.TokenManagement +{ + /// + /// Token status + /// + public enum TokenStatus + { + /// + /// Valid + /// + Valid, + /// + /// Expired token + /// + Expired + } + + /// + /// Token info + /// + public class TokenInfo + { + /// + /// The scope of the token + /// + public TokenScope Scope { get; } + /// + /// The user API key + /// + public string ApiKey { get; } + /// + /// The server token + /// + public string Token { get; } + /// + /// The timestamp the token was created + /// + public DateTime CreateTime { get; set; } + /// + /// Token status + /// + public TokenStatus Status { get; set; } + + /// + /// The time the token should be refreshed + /// + public DateTime NextRefreshTime { get; set; } + /// + /// The time until which the token is valid + /// + public DateTime ValidUntil { get; set; } + + + /// + /// The time the token is valid for + /// + public TimeSpan ValidTime { get; set; } + /// + /// The refresh interval + /// + public TimeSpan RefreshInterval { get; set; } + /// + /// How the token is managed + /// + public TokenManagementType ManagementType { get; set; } + + /// + /// Expired event + /// + public event Action? Expired; + + /// + /// ctor + /// + public TokenInfo(TokenScope scope, string token, TimeSpan refreshInterval, TimeSpan timeValid, TokenManagementType managementType) + { + Scope = scope; + ApiKey = scope.ApiKey; + Token = token; + RefreshInterval = refreshInterval; + ValidTime = timeValid; + ManagementType = managementType; + } + + internal void MarkExpired() + { + Status = TokenStatus.Expired; + NextRefreshTime = DateTime.MaxValue; + } + + internal void InvokeExpired() + { + Expired?.Invoke(this); + } + + internal void Refresh() + { + NextRefreshTime = DateTime.UtcNow.Add(RefreshInterval); + ValidUntil = DateTime.UtcNow.Add(ValidTime); + } + } +} diff --git a/CryptoExchange.Net/TokenManagement/TokenLease.cs b/CryptoExchange.Net/TokenManagement/TokenLease.cs new file mode 100644 index 00000000..652bfded --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenLease.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.TokenManagement +{ + /// + /// A token lease + /// + public class TokenLease + { + private readonly TokenRegistry _registry; + private int _released; + + internal TokenLease(TokenRegistry registry, TokenScope scope, Guid ownerId, TokenInfo token) + { + _registry = registry; + Scope = scope; + OwnerId = ownerId; + Token = token; + } + + internal Guid OwnerId { get; } + internal TokenScope Scope { get; } + + /// + /// Token info + /// + public TokenInfo Token { get; } + + /// + /// Release the lease for this token + /// + /// + public Task ReleaseAsync() + { + if (Interlocked.Exchange(ref _released, 1) == 1) + return Task.CompletedTask; + + return _registry.ReleaseAsync(this); + } + } +} diff --git a/CryptoExchange.Net/TokenManagement/TokenManagementType.cs b/CryptoExchange.Net/TokenManagement/TokenManagementType.cs new file mode 100644 index 00000000..d659036d --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenManagementType.cs @@ -0,0 +1,17 @@ +namespace CryptoExchange.Net.TokenManagement +{ + /// + /// Defines how tokens are managed + /// + public enum TokenManagementType + { + /// + /// Tokens represent an active server side resource. They are kept alive while in use and stopped when the last lease is released. + /// + Active, + /// + /// Tokens represent a temporary credential. They are cached for reuse until their validity period expires and don't require background maintenance. + /// + Cached + } +} diff --git a/CryptoExchange.Net/TokenManagement/TokenManager.cs b/CryptoExchange.Net/TokenManagement/TokenManager.cs new file mode 100644 index 00000000..92529e35 --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenManager.cs @@ -0,0 +1,71 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets.Default; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.TokenManagement +{ + /// + /// Token manager + /// + public class TokenManager + { + private readonly TokenRegistry _registry; + private readonly TokenOperations _operations; + private readonly ILogger _logger; + private readonly TimeSpan _timeValid; + private readonly TimeSpan _refreshInterval; + private readonly TokenManagementType _managementType; + private readonly TimeSpan _maintenanceInterval; + + /// + /// ctor + /// + public TokenManager( + string registryKey, + ILoggerFactory? loggerFactory, + TimeSpan refreshInterval, + TimeSpan timeValid, + Func>> startToken, + Func>? keepAliveToken = null, + Func>? stopToken = null, + TokenManagementType managementType = TokenManagementType.Active, + TimeSpan? maintenanceInterval = null) + { + _logger = loggerFactory?.CreateLogger(registryKey + "." + nameof(TokenManager)) ?? NullLogger.Instance; + _refreshInterval = refreshInterval; + _timeValid = timeValid; + _maintenanceInterval = maintenanceInterval ?? TimeSpan.FromSeconds(10); + _registry = TokenRegistryProvider.GetRegistry(registryKey, _logger, _maintenanceInterval); + _operations = new TokenOperations(startToken, keepAliveToken, stopToken); + _managementType = managementType; + } + + /// + /// Acquire a token for the provided scope + /// + public Task> AcquireAsync(TokenScope scope, CancellationToken ct = default) + => _registry.AcquireAsync(_logger, scope, _refreshInterval, _timeValid, _operations, _managementType, ct); + + /// + /// Acquire a token and replace the current token of the subscription with the new one. + /// + public async Task> AcquireAndReplaceAsync(Subscription subscription, TokenScope scope, CancellationToken ct = default) + { + var acquireResult = await _registry.AcquireAsync(_logger, scope, _refreshInterval, _timeValid, _operations, _managementType, ct).ConfigureAwait(false); + if (!acquireResult.Success) + return acquireResult; + + var oldLease = subscription.TokenLease; + subscription.TokenLease = acquireResult.Data; + if (oldLease != null) + await oldLease.ReleaseAsync().ConfigureAwait(false); + + return acquireResult; + } + } + +} diff --git a/CryptoExchange.Net/TokenManagement/TokenOperations.cs b/CryptoExchange.Net/TokenManagement/TokenOperations.cs new file mode 100644 index 00000000..e01d7355 --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenOperations.cs @@ -0,0 +1,26 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.TokenManagement +{ + internal class TokenOperations + { + public TokenOperations( + Func>> startToken, + Func>? keepAliveToken, + Func>? stopToken) + { + StartToken = startToken; + KeepAliveToken = keepAliveToken; + StopToken = stopToken; + } + + public Func>> StartToken { get; } + public Func>? KeepAliveToken { get; } + public Func>? StopToken { get; } + } +} diff --git a/CryptoExchange.Net/TokenManagement/TokenRegistry.cs b/CryptoExchange.Net/TokenManagement/TokenRegistry.cs new file mode 100644 index 00000000..af1835c9 --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenRegistry.cs @@ -0,0 +1,324 @@ +using CryptoExchange.Net.Objects; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.TokenManagement +{ + internal class TokenRegistry + { + private readonly string _registryKey; + private readonly ILogger _logger; + private readonly TimeSpan _maintenanceInterval; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly Dictionary _tokens = new Dictionary(); + + private CancellationTokenSource? _keepAliveCts; + private Task? _keepAliveTask; + + public TokenRegistry(string registryKey, ILogger logger, TimeSpan maintenanceInterval) + { + _registryKey = registryKey; + _logger = logger; + _maintenanceInterval = maintenanceInterval; + } + + public async Task> AcquireAsync( + ILogger logger, + TokenScope scope, + TimeSpan refreshInterval, + TimeSpan timeValid, + TokenOperations operations, + TokenManagementType managementType, + CancellationToken ct) + { + if (string.IsNullOrEmpty(scope.ApiKey)) + return CallResult.Fail(new NoApiCredentialsError()); + + _logger.LogTrace("Acquiring token lease for scope {Scope}", scope.ToString()); + + ManagedToken? existing; + var ownerId = Guid.NewGuid(); + + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + try + { + var now = DateTime.UtcNow; + if (_tokens.TryGetValue(scope.Id, out existing)) + { + if (existing.Info.Status == TokenStatus.Valid && existing.Info.ValidUntil > now) + { + _logger.LogTrace("Existing token found for scope {Scope}, now {Count} leases", scope.ToString(), existing.RefCount + 1); + existing.RefCount++; + existing.Owners[ownerId] = operations; + + EnsureKeepAliveLoop(logger); + return CallResult.Ok(new TokenLease(this, scope, ownerId, existing.Info)); + } + + _tokens.Remove(scope.Id); + existing.Info.MarkExpired(); + } + } + finally + { + _semaphore.Release(); + } + + _logger.LogDebug("Starting new token for scope {Scope}", scope.ToString()); + var startResult = await operations.StartToken(scope, ct).ConfigureAwait(false); + if (!startResult.Success) + { + _logger.LogDebug("Failed to start new token for scope {Scope}", scope.ToString()); + return CallResult.Fail(startResult.Error!); + } + + try + { + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + } + catch(OperationCanceledException) + { + if (operations.StopToken != null) + _ = operations.StopToken(new TokenInfo(scope, startResult.Data, refreshInterval, timeValid, managementType), CancellationToken.None); + return CallResult.Fail(new CancellationRequestedError()); + } + + try + { + // Another client may have started the same token while this call was in flight. + if (_tokens.TryGetValue(scope.Id, out existing) + && existing.Info.Status == TokenStatus.Valid + && existing.Info.ValidUntil > DateTime.UtcNow) + { + _logger.LogDebug("Duplicate token found for scope {Scope}, keeping existing token", scope.ToString()); + existing.RefCount++; + existing.Owners[ownerId] = operations; + + // The server may have returned the same token. We keep the first tracked instance + // and stop the just-created duplicate only if it differs. + if (existing.Info.Token != startResult.Data && operations.StopToken != null) + _ = operations.StopToken(new TokenInfo(scope, startResult.Data, refreshInterval, timeValid, managementType), CancellationToken.None); + + EnsureKeepAliveLoop(logger); + return CallResult.Ok(new TokenLease(this, scope, ownerId, existing.Info)); + } + + var info = new TokenInfo(scope, startResult.Data, refreshInterval, timeValid, managementType) + { + CreateTime = DateTime.UtcNow, + NextRefreshTime = DateTime.UtcNow.Add(refreshInterval), + ValidUntil = DateTime.UtcNow.Add(timeValid) + }; + + var token = new ManagedToken(info) + { + RefCount = 1 + }; + + token.Owners[ownerId] = operations; + _tokens[scope.Id] = token; + + EnsureKeepAliveLoop(logger); + _logger.LogTrace("Token lease for {Scope}, token {Token} acquired, valid until {ValidUntil}", scope.ToString(), info.Token, info.ValidUntil); + return CallResult.Ok(new TokenLease(this, scope, ownerId, info)); + } + finally + { + _semaphore.Release(); + } + } + + public async Task ReleaseAsync(TokenLease lease) + { + ManagedToken? tokenToStop = null; + TokenOperations? stopOperations = null; + + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + if (!_tokens.TryGetValue(lease.Scope.Id, out var token)) + return; + + if (!token.Owners.TryGetValue(lease.OwnerId, out stopOperations)) + return; // Lease belongs to an old/replaced token; don't touch current token. + + _logger.LogTrace("Releasing token lease for {Scope}, token {Token}. {Count} leases left", lease.Scope.ToString(), lease.Token.Token, token.RefCount - 1); + + token.Owners.Remove(lease.OwnerId); + token.RefCount--; + if (token.RefCount > 0) + return; + + if (token.Info.ManagementType == TokenManagementType.Active) + { + _tokens.Remove(lease.Scope.Id); + tokenToStop = token; + } + else if (token.Info.ValidUntil <= DateTime.UtcNow) + { + _tokens.Remove(lease.Scope.Id); + token.Info.MarkExpired(); + } + + if (!HasTokensRequiringKeepAlive()) + StopKeepAliveLoop(); + } + finally + { + _semaphore.Release(); + } + + if (tokenToStop != null && stopOperations?.StopToken != null) + { + _logger.LogDebug("No token leases left for token {Token} for scope {Scope}, stopping", lease.Token.Token.ToString(), lease.Scope.ToString()); + await stopOperations.StopToken(tokenToStop.Info, CancellationToken.None).ConfigureAwait(false); + } + } + + private void EnsureKeepAliveLoop(ILogger logger) + { + if (!HasTokensRequiringKeepAlive()) + return; + + if (_keepAliveTask != null && !_keepAliveTask.IsCompleted) + return; + + _keepAliveCts = new CancellationTokenSource(); + _keepAliveTask = Task.Run(() => ProcessKeepAlivesAsync(logger, _keepAliveCts.Token)); + } + + private void StopKeepAliveLoop() + { + _logger.LogDebug("Stopping keep alive loop"); + _keepAliveCts?.Cancel(); + _keepAliveCts?.Dispose(); + _keepAliveCts = null; + _keepAliveTask = null; + } + + private async Task ProcessKeepAlivesAsync(ILogger logger, CancellationToken ct) + { + logger.LogDebug("Starting keep alive loop"); + + while (!ct.IsCancellationRequested) + { + try + { + await Task.Delay(_maintenanceInterval, ct).ConfigureAwait(false); + + List<(ManagedToken Token, TokenOperations Operations)> dueTokens; + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + try + { + var now = DateTime.UtcNow; + if (!HasTokensRequiringKeepAlive()) + { + logger.LogDebug("No tokens require keep alive, stopping keep alive loop"); + StopKeepAliveLoop(); + } + + dueTokens = _tokens.Values + .Where(x => x.Info.ManagementType == TokenManagementType.Active && x.RefCount > 0 && x.Info.NextRefreshTime <= now) + .Select(x => (Token: x, Operations: x.Owners.Values.FirstOrDefault(o => o.KeepAliveToken != null))) + .Where(x => x.Operations != null) + .Select(x => (x.Token, x.Operations!)) + .ToList(); + + } + finally + { + _semaphore.Release(); + } + + if (dueTokens.Count > 0) + logger.LogTrace("Keeping alive {Count} tokens", dueTokens.Count); + + var expiredTokens = new List(); + foreach (var item in dueTokens) + { + var result = await item.Operations.KeepAliveToken!(item.Token.Info, ct).ConfigureAwait(false); + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + try + { + if (!_tokens.TryGetValue(item.Token.Info.Scope.Id, out var current) + || !ReferenceEquals(current, item.Token)) + { + continue; + } + + if (result.Success) + { + logger.LogDebug("Token {Token} for {Scope} successfully kept alive", current.Info.Token, current.Info.Scope); + current.Info.Refresh(); + } + else + { + logger.LogWarning("Token {Token} for {Scope} keep alive failed: {Error}", current.Info.Token, current.Info.Scope, result.Error); + if (current.Info.ValidUntil <= DateTime.UtcNow) + { + _tokens.Remove(current.Info.Scope.Id); + + current.Info.MarkExpired(); + expiredTokens.Add(current.Info); + + if (_tokens.Count == 0) + { + logger.LogDebug("All tokens expired, stopping keep alive loop"); + StopKeepAliveLoop(); + } + } + else + { + // TODO Some smarter way to determine next refresh time based on the remaining valid time + current.Info.NextRefreshTime = DateTime.UtcNow.AddSeconds(Math.Min(TimeSpan.FromMinutes(5).TotalSeconds, current.Info.RefreshInterval.TotalSeconds)); + } + } + } + finally + { + _semaphore.Release(); + } + } + + foreach (var expiredToken in expiredTokens) + expiredToken.InvokeExpired(); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Keep alive loop error"); + } + } + + logger.LogTrace("Keep alive loop stopped"); + } + + private sealed class ManagedToken + { + public ManagedToken(TokenInfo info) + { + Info = info; + } + + public TokenInfo Info { get; } + public int RefCount { get; set; } + public Dictionary Owners { get; } = new Dictionary(); + } + + private bool HasTokensRequiringKeepAlive() + { + return _tokens.Values.Any(x => + x.Info.ManagementType == TokenManagementType.Active + && x.RefCount > 0 + && x.Owners.Values.Any(o => o.KeepAliveToken != null)); + } + } +} diff --git a/CryptoExchange.Net/TokenManagement/TokenRegistryProvider.cs b/CryptoExchange.Net/TokenManagement/TokenRegistryProvider.cs new file mode 100644 index 00000000..b1334fa3 --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenRegistryProvider.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.TokenManagement +{ + internal static class TokenRegistryProvider + { + private static readonly object _lock = new object(); + private static readonly Dictionary _registries = new Dictionary(); + + public static TokenRegistry GetRegistry(string registryKey, ILogger logger, TimeSpan maintenanceInterval) + { + lock (_lock) + { + if (!_registries.TryGetValue(registryKey, out var registry)) + { + registry = new TokenRegistry(registryKey, logger, maintenanceInterval); + _registries[registryKey] = registry; + } + + return registry; + } + } + } +} diff --git a/CryptoExchange.Net/TokenManagement/TokenScope.cs b/CryptoExchange.Net/TokenManagement/TokenScope.cs new file mode 100644 index 00000000..9dc4006d --- /dev/null +++ b/CryptoExchange.Net/TokenManagement/TokenScope.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.TokenManagement +{ + /// + /// Token scope + /// + public class TokenScope + { + /// + /// Exchange name + /// + public string Exchange { get; } + /// + /// Environment name + /// + public string Environment { get; } + /// + /// Token type + /// + public string TokenType { get; } + /// + /// API key + /// + public string ApiKey { get; } + /// + /// Additional identifier + /// + public string? AdditionalIdentifier { get; } + + private readonly string _maskedKey = ""; + + /// + /// The scope identifier + /// + public string Id { get; } + + /// + /// ctor + /// + public TokenScope( + string exchange, + string environment, + string tokenType, + string apiKey, + string? additionalIdentifier = null) + { + Exchange = exchange; + Environment = environment; + TokenType = tokenType; + ApiKey = apiKey; + AdditionalIdentifier = additionalIdentifier; + + Id = $"{Exchange}/{Environment}/{TokenType}/{ApiKey}/{AdditionalIdentifier}"; + + if (apiKey.Length > 12) + _maskedKey = apiKey.Substring(0, 3) + "***" + apiKey.Substring(apiKey.Length - 4, 3); + else + _maskedKey = "******"; + } + + + /// + public override string ToString() => $"{Exchange}/{Environment}/{TokenType}/{_maskedKey}/{AdditionalIdentifier}"; + } +} diff --git a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs index f6be4867..65ba81f1 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs @@ -186,11 +186,11 @@ namespace CryptoExchange.Net.Trackers.Klines AddOrUpdate(update.Data); }).ConfigureAwait(false); - if (!subResult) + if (!subResult.Success) { _logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); Status = SyncStatus.Disconnected; - return subResult; + return CallResult.Fail(subResult.Error!); } _updateSubscription = subResult.Data; @@ -199,16 +199,16 @@ namespace CryptoExchange.Net.Trackers.Klines _updateSubscription.ConnectionRestored += HandleConnectionRestored; var startResult = await DoStartAsync().ConfigureAwait(false); - if (!startResult) + if (!startResult.Success) { _ = subResult.Data.CloseAsync(); Status = SyncStatus.Disconnected; - return new CallResult(startResult.Error!); + return CallResult.Fail(startResult.Error!); } Status = SyncStatus.Synced; _logger.KlineTrackerStarted(SymbolName); - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -229,7 +229,7 @@ namespace CryptoExchange.Net.Trackers.Klines protected virtual async Task DoStartAsync() { if (!_startWithSnapshot) - return CallResult.SuccessResult; + return CallResult.Ok(); var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value); if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime) @@ -241,8 +241,8 @@ namespace CryptoExchange.Net.Trackers.Klines var data = new List(); await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false)) { - if (!result) - return result; + if (!result.Success) + return CallResult.Fail(result.Error!); if (Limit != null && data.Count > Limit) break; @@ -251,7 +251,7 @@ namespace CryptoExchange.Net.Trackers.Klines } SetInitialData(data); - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -449,7 +449,7 @@ namespace CryptoExchange.Net.Trackers.Klines return; var resyncResult = await DoStartAsync().ConfigureAwait(false); - success = resyncResult; + success = resyncResult.Success; } _logger.KlineTrackerConnectionRestored(SymbolName); diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 6a7e0095..a03f1f76 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -209,11 +209,11 @@ namespace CryptoExchange.Net.Trackers.Trades AddData(update.Data); }).ConfigureAwait(false); - if (!subResult) + if (!subResult.Success) { _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); Status = SyncStatus.Disconnected; - return subResult; + return CallResult.Fail(subResult.Error); } _updateSubscription = subResult.Data; @@ -222,7 +222,7 @@ namespace CryptoExchange.Net.Trackers.Trades _updateSubscription.ConnectionRestored += HandleConnectionRestored; var result = await DoStartAsync().ConfigureAwait(false); - if (!result) + if (!result.Success) { _ = subResult.Data.CloseAsync(); Status = SyncStatus.Disconnected; @@ -231,7 +231,7 @@ namespace CryptoExchange.Net.Trackers.Trades SetSyncStatus(); _logger.TradeTrackerStarted(SymbolName); - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -252,7 +252,7 @@ namespace CryptoExchange.Net.Trackers.Trades protected virtual async Task DoStartAsync() { if (!_startWithSnapshot) - return CallResult.SuccessResult; + return CallResult.Ok(); if (_historyRestClient != null) { @@ -261,8 +261,8 @@ namespace CryptoExchange.Net.Trackers.Trades var data = new List(); await foreach (var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false)) { - if (!result) - return result; + if (!result.Success) + return CallResult.Fail(result.Error); if (Limit != null && data.Count > Limit) break; @@ -279,15 +279,15 @@ namespace CryptoExchange.Net.Trackers.Trades limit = Math.Min(_recentRestClient.GetRecentTradesOptions.MaxLimit, Limit.Value); var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false); - if (!snapshot) + if (!snapshot.Success) { - return snapshot; + return CallResult.Fail(snapshot.Error); } SetInitialData(snapshot.Data); } - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -463,7 +463,7 @@ namespace CryptoExchange.Net.Trackers.Trades return; var resyncResult = await DoStartAsync().ConfigureAwait(false); - success = resyncResult; + success = resyncResult.Success; } _logger.TradeTrackerConnectionRestored(SymbolName); diff --git a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs index e98282c5..4682454f 100644 --- a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs @@ -2,6 +2,8 @@ using CryptoExchange.Net.Trackers.UserData.Objects; using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.UserData.Interfaces @@ -30,5 +32,11 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces /// On data update /// event Func, Task>? OnUpdate; + + /// + /// Stream updates as they are received + /// + /// Cancellation token + IAsyncEnumerable> StreamUpdatesAsync(CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs index fcc669ec..ee733bde 100644 --- a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.Trackers.UserData.Objects; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.UserData.Interfaces @@ -59,7 +60,7 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces /// /// Start tracking user data /// - Task StartAsync(); + Task StartAsync(CancellationToken ct = default); /// /// Stop tracking data /// diff --git a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs index de498bd0..35063882 100644 --- a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.Trackers.UserData.Objects; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.UserData.Interfaces @@ -27,6 +28,11 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces /// public string Exchange { get; } + /// + /// Whether the tracker was started + /// + public bool Started { get; } + /// /// Currently tracked symbols. Data for these symbols will be requested when polling. /// Websocket updates will be available for all symbols regardless. @@ -55,7 +61,7 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces /// /// Start tracking user data /// - Task StartAsync(); + Task StartAsync(CancellationToken ct = default); /// /// Stop tracking data /// diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs index 3895b879..d544e31a 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs @@ -65,10 +65,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers protected override bool? CheckIfUpdateShouldBeApplied(SharedBalance existingItem, SharedBalance updateItem) => true; /// - protected override Task> DoSubscribeAsync(string? listenKey) + protected override Task> DoSubscribeAsync() { if (_socketClient == null) - return Task.FromResult(new CallResult(data: null)); + return Task.FromResult(new WebSocketResult(default!, default!, default)); var accountType = _accountType == SharedAccountType.Spot ? TradingMode.Spot : _accountType == SharedAccountType.PerpetualInverseFutures ? TradingMode.PerpetualInverse : @@ -76,7 +76,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers _accountType == SharedAccountType.DeliveryInverseFutures ? TradingMode.DeliveryInverse : TradingMode.PerpetualLinear; return ExchangeHelpers.ProcessQueuedAsync( - async handler => await _socketClient.SubscribeToBalanceUpdatesAsync(new SubscribeBalancesRequest(listenKey, accountType, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), + async handler => await _socketClient.SubscribeToBalanceUpdatesAsync(new SubscribeBalancesRequest(accountType, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), x => HandleUpdateAsync(UpdateSource.Push, x.Data))!; } diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs index 943000f7..28cb5093 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs @@ -45,7 +45,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers _socketClient = socketClient; _exchangeParameters = exchangeParameters; - _requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol"); + _requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Names.Contains("Symbol")); } internal void ClearDataForSymbol(SharedSymbol symbol) @@ -215,13 +215,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } /// - protected override Task> DoSubscribeAsync(string? listenKey) + protected override Task> DoSubscribeAsync() { if (_socketClient == null) - return Task.FromResult(new CallResult(data: null)); + return Task.FromResult(new WebSocketResult(_exchange, default!, default)); return ExchangeHelpers.ProcessQueuedAsync( - async handler => await _socketClient.SubscribeToFuturesOrderUpdatesAsync(new SubscribeFuturesOrderRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), + async handler => await _socketClient.SubscribeToFuturesOrderUpdatesAsync(new SubscribeFuturesOrderRequest(exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), x => HandleUpdateAsync(UpdateSource.Push, x.Data))!; } diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs index 6a77e6c1..a29d7910 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs @@ -144,13 +144,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } /// - protected override Task> DoSubscribeAsync(string? listenKey) + protected override Task> DoSubscribeAsync() { if (_socketClient == null) - return Task.FromResult(new CallResult(data: null)); + return Task.FromResult(new WebSocketResult(_exchange!, default!, default)); return ExchangeHelpers.ProcessQueuedAsync( - async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), + async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), x => HandleUpdateAsync(UpdateSource.Push, x.Data))!; } diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs index 092c01f2..e4dbb7c7 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs @@ -206,13 +206,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers protected override bool? CheckIfUpdateShouldBeApplied(SharedPosition existingItem, SharedPosition updateItem) => true; /// - protected override Task> DoSubscribeAsync(string? listenKey) + protected override Task> DoSubscribeAsync() { if (_socketClient == null) - return Task.FromResult(new CallResult(data: null)); + return Task.FromResult(new WebSocketResult(_exchange!, default!, default)); return ExchangeHelpers.ProcessQueuedAsync( - async handler => await _socketClient.SubscribeToPositionUpdatesAsync(new SubscribePositionRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), + async handler => await _socketClient.SubscribeToPositionUpdatesAsync(new SubscribePositionRequest(exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), x => HandleUpdateAsync(UpdateSource.Push, x.Data))!; } diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs index 5feeedcd..ccddf880 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs @@ -45,7 +45,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers _socketClient = socketClient; _exchangeParameters = exchangeParameters; - _requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol"); + _requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Names.Contains("Symbol")); } internal void ClearDataForSymbol(SharedSymbol symbol) @@ -225,13 +225,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } /// - protected override Task> DoSubscribeAsync(string? listenKey) + protected override Task> DoSubscribeAsync() { if (_socketClient == null) - return Task.FromResult(new CallResult(data: null)); + return Task.FromResult(new WebSocketResult(_exchange!, default!, default)); return ExchangeHelpers.ProcessQueuedAsync( - async handler => await _socketClient.SubscribeToSpotOrderUpdatesAsync(new SubscribeSpotOrderRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), + async handler => await _socketClient.SubscribeToSpotOrderUpdatesAsync(new SubscribeSpotOrderRequest(exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), x => HandleUpdateAsync(UpdateSource.Push, x.Data))!; } diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs index 4157e3cb..15be314f 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs @@ -140,13 +140,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } /// - protected override Task> DoSubscribeAsync(string? listenKey) + protected override Task> DoSubscribeAsync() { if (_socketClient == null) - return Task.FromResult(new CallResult(data: null)); + return Task.FromResult(new WebSocketResult(_exchange!, default!, default)); return ExchangeHelpers.ProcessQueuedAsync( - async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), + async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false), x => HandleUpdateAsync(UpdateSource.Push, x.Data))!; } diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs index 77f8de1f..b9db2fc4 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs @@ -8,7 +8,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers @@ -130,8 +132,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers /// /// Start the tracker /// - /// Optional listen key - public abstract Task StartAsync(string? listenKey); + public abstract Task StartAsync(CancellationToken ct = default); /// /// Stop the tracker @@ -264,52 +265,83 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } /// - public async override Task StartAsync(string? listenKey) + public async override Task StartAsync(CancellationToken ct = default) { _startTime = DateTime.UtcNow; _cts = new CancellationTokenSource(); - var start = await SubscribeAsync(listenKey).ConfigureAwait(false); - if (!start) + var start = await SubscribeAsync(ct).ConfigureAwait(false); + if (!start.Success) return start; Connected = true; _pollTask = PollAsync(); - await _initialPollDoneEvent.WaitAsync().ConfigureAwait(false); + await _initialPollDoneEvent.WaitAsync(ct: ct).ConfigureAwait(false); if (_initialPollingError != null) { await StopAsync().ConfigureAwait(false); - return new CallResult(_initialPollingError); + return CallResult.Fail(_initialPollingError); } - return CallResult.SuccessResult; + return CallResult.Ok(); + } + + /// + public async IAsyncEnumerable> StreamUpdatesAsync([EnumeratorCancellation] CancellationToken ct = default) + { + var updateQueue = Channel.CreateUnbounded>(); + async Task OnUpdateHandler(UserDataUpdate update) + { + await updateQueue.Writer.WriteAsync(update, ct).ConfigureAwait(false); + } + OnUpdate += OnUpdateHandler; + try + { + while (!ct.IsCancellationRequested) + { + UserDataUpdate update; + try + { + update = await updateQueue.Reader.ReadAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException) { break; } + yield return update; + } + } + finally + { + OnUpdate -= OnUpdateHandler; + } } /// /// Subscribe the websocket /// - public async Task SubscribeAsync(string? listenKey) + public async Task SubscribeAsync(CancellationToken ct = default) { - var subscriptionResult = await DoSubscribeAsync(listenKey).ConfigureAwait(false); - if (!subscriptionResult) + if (ct.IsCancellationRequested) + return CallResult.Fail(new CancellationRequestedError()); + + var subscriptionResult = await DoSubscribeAsync().ConfigureAwait(false); + if (!subscriptionResult.Success) { // Failed // .. - return subscriptionResult; + return CallResult.Fail(subscriptionResult.Error); } if (subscriptionResult.Data == null) { // No subscription available // .. - return CallResult.SuccessResult; + return CallResult.Ok(); } _subscription = subscriptionResult.Data; _subscription.SubscriptionStatusChanged += SubscriptionStatusChanged; - return CallResult.SuccessResult; + return CallResult.Ok(); } /// @@ -410,7 +442,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers /// /// Websocket subscription implementation /// - protected abstract Task> DoSubscribeAsync(string? listenKey); + protected abstract Task> DoSubscribeAsync(); /// /// Polling task diff --git a/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs index e7e0b021..d66305cd 100644 --- a/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs @@ -21,10 +21,6 @@ namespace CryptoExchange.Net.Trackers.UserData /// protected readonly ILogger _logger; /// - /// Listen key to use for subscriptions - /// - protected string? _listenKey; - /// /// Cts /// protected CancellationTokenSource? _cts; @@ -42,6 +38,9 @@ namespace CryptoExchange.Net.Trackers.UserData /// public string? UserIdentifier { get; } + /// + public bool Started { get; protected set; } + /// /// Connected status changed /// @@ -81,20 +80,24 @@ namespace CryptoExchange.Net.Trackers.UserData /// /// Start the data tracker /// - public async Task StartAsync() + public async Task StartAsync(CancellationToken ct) { + if (ct.IsCancellationRequested) + return CallResult.Fail(new CancellationRequestedError()); + _cts = new CancellationTokenSource(); + Started = true; foreach(var tracker in DataTrackers) tracker.OnConnectedChange += (x) => OnConnectedChange?.Invoke(tracker.DataType, x); - var result = await DoStartAsync().ConfigureAwait(false); - if (!result) + var result = await DoStartAsync(ct).ConfigureAwait(false); + if (!result.Success) return result; var tasks = new List>(); foreach (var dataTracker in DataTrackers) - tasks.Add(dataTracker.StartAsync(_listenKey)); + tasks.Add(dataTracker.StartAsync(ct)); await Task.WhenAll(tasks).ConfigureAwait(false); if (!tasks.All(x => x.Result.Success)) @@ -103,13 +106,13 @@ namespace CryptoExchange.Net.Trackers.UserData return tasks.First(x => !x.Result.Success).Result; } - return CallResult.SuccessResult; + return CallResult.Ok(); } /// /// Implementation specific start logic /// - protected abstract Task DoStartAsync(); + protected abstract Task DoStartAsync(CancellationToken ct = default); /// /// Stop the data tracker diff --git a/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs index b7366c59..f83f01eb 100644 --- a/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs @@ -1,14 +1,15 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Trackers.UserData.Interfaces; +using CryptoExchange.Net.Trackers.UserData.ItemTrackers; +using CryptoExchange.Net.Trackers.UserData.Objects; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; using System.Linq; -using CryptoExchange.Net.Trackers.UserData.ItemTrackers; -using CryptoExchange.Net.Trackers.UserData.Interfaces; -using CryptoExchange.Net.Trackers.UserData.Objects; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.UserData { @@ -18,10 +19,8 @@ namespace CryptoExchange.Net.Trackers.UserData public abstract class UserFuturesDataTracker : UserDataTracker, IUserFuturesDataTracker { private readonly IFuturesSymbolRestClient _symbolClient; - private readonly IListenKeyRestClient? _listenKeyClient; private readonly ExchangeParameters? _exchangeParameters; private readonly TradingMode _tradingMode; - private Task? _lkKeepAliveTask; /// protected override UserDataItemTracker[] DataTrackers { get; } @@ -53,7 +52,6 @@ namespace CryptoExchange.Net.Trackers.UserData public UserFuturesDataTracker( ILogger logger, IFuturesSymbolRestClient symbolRestClient, - IListenKeyRestClient? listenKeyRestClient, IBalanceRestClient balanceRestClient, IBalanceSocketClient? balanceSocketClient, IFuturesOrderRestClient futuresOrderRestClient, @@ -67,7 +65,6 @@ namespace CryptoExchange.Net.Trackers.UserData { // create trackers _symbolClient = symbolRestClient; - _listenKeyClient = listenKeyRestClient; _exchangeParameters = exchangeParameters; _tradingMode = accountType == SharedAccountType.PerpetualInverseFutures ? TradingMode.PerpetualInverse : @@ -103,56 +100,16 @@ namespace CryptoExchange.Net.Trackers.UserData } /// - protected override async Task DoStartAsync() + protected override async Task DoStartAsync(CancellationToken ct = default) { - var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(_tradingMode, exchangeParameters: _exchangeParameters)).ConfigureAwait(false); - if (!symbolResult) + var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(_tradingMode, exchangeParameters: _exchangeParameters), ct).ConfigureAwait(false); + if (!symbolResult.Success) { _logger.LogWarning("Failed to start UserFuturesDataTracker; symbols request failed: {Error}", symbolResult.Error); - return symbolResult; + return CallResult.Fail(symbolResult.Error); } - if (_listenKeyClient != null) - { - var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(_tradingMode, exchangeParameters: _exchangeParameters)).ConfigureAwait(false); - if (!lkResult) - { - _logger.LogWarning("Failed to start UserFuturesDataTracker; listen key request failed: {Error}", lkResult.Error); - return lkResult; - } - - _lkKeepAliveTask = KeepAliveListenKeyAsync(); - - _listenKey = lkResult.Data; - } - - return CallResult.SuccessResult; - } - - /// - protected override async Task DoStopAsync() - { - if (_lkKeepAliveTask != null) - await _lkKeepAliveTask.ConfigureAwait(false); - } - - private async Task KeepAliveListenKeyAsync() - { - var interval = TimeSpan.FromMinutes(30); - while (!_cts!.IsCancellationRequested) - { - try { await Task.Delay(interval, _cts.Token).ConfigureAwait(false); } catch (Exception) - { - break; - } - - var result = await _listenKeyClient!.KeepAliveListenKeyAsync(new KeepAliveListenKeyRequest(_listenKey!, _tradingMode)).ConfigureAwait(false); - if (!result) - _logger.LogWarning("Listen key keep alive failed: " + result.Error); - - // If failed shorten the delay to allow a couple more retries - interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5); - } + return CallResult.Ok(); } /// diff --git a/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs index b71a7006..8180c990 100644 --- a/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs @@ -1,13 +1,14 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; -using Microsoft.Extensions.Logging; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Linq; using CryptoExchange.Net.Trackers.UserData.Interfaces; -using CryptoExchange.Net.Trackers.UserData.Objects; using CryptoExchange.Net.Trackers.UserData.ItemTrackers; +using CryptoExchange.Net.Trackers.UserData.Objects; +using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace CryptoExchange.Net.Trackers.UserData { @@ -17,9 +18,7 @@ namespace CryptoExchange.Net.Trackers.UserData public class UserSpotDataTracker : UserDataTracker, IUserSpotDataTracker { private readonly ISpotSymbolRestClient _symbolClient; - private readonly IListenKeyRestClient? _listenKeyClient; private readonly ExchangeParameters? _exchangeParameters; - private Task? _lkKeepAliveTask; /// protected override UserDataItemTracker[] DataTrackers { get; } @@ -36,7 +35,6 @@ namespace CryptoExchange.Net.Trackers.UserData public UserSpotDataTracker( ILogger logger, ISpotSymbolRestClient symbolRestClient, - IListenKeyRestClient? listenKeyRestClient, IBalanceRestClient balanceRestClient, IBalanceSocketClient? balanceSocketClient, ISpotOrderRestClient spotOrderRestClient, @@ -48,7 +46,6 @@ namespace CryptoExchange.Net.Trackers.UserData { // create trackers _symbolClient = symbolRestClient; - _listenKeyClient = listenKeyRestClient; _exchangeParameters = exchangeParameters; var trackers = new List(); @@ -75,57 +72,16 @@ namespace CryptoExchange.Net.Trackers.UserData } /// - protected override async Task DoStartAsync() + protected override async Task DoStartAsync(CancellationToken ct = default) { - var symbolResult = await _symbolClient.GetSpotSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false); - if (!symbolResult) + var symbolResult = await _symbolClient.GetSpotSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters), ct).ConfigureAwait(false); + if (!symbolResult.Success) { _logger.LogWarning("Failed to start UserSpotDataTracker; symbols request failed: {Error}", symbolResult.Error); - return symbolResult; + return CallResult.Fail(symbolResult.Error); } - if (_listenKeyClient != null) - { - var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false); - if (!lkResult) - { - _logger.LogWarning("Failed to start UserSpotDataTracker; listen key request failed: {Error}", lkResult.Error); - return lkResult; - } - - _lkKeepAliveTask = KeepAliveListenKeyAsync(); - - _listenKey = lkResult.Data; - } - - return CallResult.SuccessResult; - } - - /// - protected override async Task DoStopAsync() - { - if (_lkKeepAliveTask != null) - await _lkKeepAliveTask.ConfigureAwait(false); - } - - private async Task KeepAliveListenKeyAsync() - { - var interval = TimeSpan.FromMinutes(30); - while (!_cts!.IsCancellationRequested) - { - try { await Task.Delay(interval, _cts.Token).ConfigureAwait(false); } - catch (Exception) - { - break; - } - - var result = await _listenKeyClient!.KeepAliveListenKeyAsync(new KeepAliveListenKeyRequest(_listenKey!, TradingMode.Spot)).ConfigureAwait(false); - if (!result) - _logger.LogWarning("Listen key keep alive failed: " + result.Error); - - // If failed shorten the delay to allow a couple more retries - interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5); - } + return CallResult.Ok(); } /// diff --git a/README.md b/README.md index 141a4eec..fb9bcb01 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net [![.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.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge) +![Since](https://img.shields.io/badge/since-2018-brightgreen?style=for-the-badge) 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. Note that the CryptoExchange.Net package itself can not be used directly for accessing API's. Either install a client library from the list below or use [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes access to all exchange API's. @@ -85,6 +86,43 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d ### Sponsor Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). +## Update notes from version 11.x to 12.x for client implementations +* Result types: + * (Web)CallResult types are replaced by HttpResult, WebSocketResult and QueryResult + * Use (Http/WebSocket/Query)Result.Ok(..) and .Fail(..) for creation + * Result objects no longer override implicit conversion to bool, use Success property instead + * CallResult.SuccessResult has been replaced with CallResult.Ok() + +* Parameters & serialization: + * ParameterCollection type is replaced by Parameters, most AddXX() methods can be replaced by Add() + * Parameter serialization behavior can be controlled in the Add method as third parameter, or in the ParameterSerializationSettings + * ArraySerialization has been move into the new Parameters object + * RestRequestConfiguration in AuthenticationProvider.ProcessRequest now contains the full RequestDefinition instead of copied fields. This changes for example `request.Authenticated` to `request.RequestDefinition.Authenticated` + +WebSocket routing: + * MessageRouting has been split into event and query routing; use CreateForEvent for subscriptions and CreateForQuery for queries. + * Queries returning a mapped type can specify a second type parameter in CreateForQuery for the result type + * MessageRouter.CreateWithoutHandler has been replaced with CreateVoid + +Shared APIs: + * Option defintions now always require the exchange name as first parameter + * Every request/subscription now has a dedicated options type + * ExchangeSymbolCache now requires EnvironmentName as parameter for operations + * Validation has been unified via `SharedClient.[Request]Options.ValidateRequest(request, this);` + * Validation now includes auth check and klines support internally, no need for explicit checks + * AsExchangeResult/ExchangeWebResult has been removed, use normal HttpResults instead + * ExchangeResult has been replaced by ExchangeCallResult + * TradingMode has been removed from the response model, only maintained on models where it makes sense + * IListenKey support has been removed, listen keys should be managed internally with TokenManager + +Various: + * ApiClients now require an exchange name in the constructor + * ApiClients now required ILoggerFactory parameter instead of ILogger instance + * RestApiClient SendAsync without type parameter removed, use SendAsync instead + * Address parameter removed from SendAsync RestApiClient, should be specified on the request definition instead + * SymbolOrderBook DoResyncAsync now returns CallResult instead of CallResult which was redundant + * PlatformInfo now required support environment names in the constructor + ## Release notes * Version 11.2.2 - 08 Jun 2026 * Fixed timing issue causing websocket connection to possible loop in error state diff --git a/llms.txt b/llms.txt index c0dfd793..40839458 100644 --- a/llms.txt +++ b/llms.txt @@ -2,7 +2,7 @@ > Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of. -CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `WebCallResult` result pattern, same WebSocket subscription model, same DI registration, same shared interfaces across all exchanges. Current version: 11.x. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported. +CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult` REST result pattern, same `WebSocketResult` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.x. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported. The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.