1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-04 18:33:39 +00:00

Compare commits

...

204 Commits

Author SHA1 Message Date
Jkorf bda81d8170 Updated to version 13.0.0 2026-09-23 10:07:50 +02:00
Jkorf 666ca76776 Fixed incorrect example 2026-09-23 09:47:15 +02:00
Jan Korf ae7504c26c Shared API's V2 (#289)
Added V2 Shared API's infrastructure
Added request coalescing
Added rate limiting admission logic
Fixed some rate limiting issues
2026-09-23 09:15:39 +02:00
Maxxx555 cecfbcba48 ArrayConverter: parse string decimals in scientific notation (#292) 2026-09-21 11:54:15 +02:00
Jkorf 4a4930d658 Updated to version 12.5.1 2026-09-01 09:03:50 +02:00
nils2525 ca245959a2 Fix subscription query completion for topic-filtered routes (#288) 2026-08-31 21:07:33 +02:00
Jkorf ff43d202d4 Add Tapbit to examples 2026-08-24 11:52:37 +02:00
Jkorf d1947e643b Added Tapbit reference 2026-08-24 10:55:38 +02:00
JKorf 6fec65cd6a Fixed failed ISymbolOrderBook start potentially leaving a pending background task 2026-08-23 20:36:47 +02:00
JKorf 053bcf6ecc Fixed caching issue for auth requests 2026-08-23 20:22:10 +02:00
JKorf 6957ba44f5 Fixed KlineTracker reporting incorrect High/Low price on GetStats result 2026-08-23 20:21:54 +02:00
Jkorf c1af97dd4a Updated to version 12.5.0 2026-08-21 10:21:54 +02:00
Jkorf ecfe3cf392 Added ISpotOrderManagementSocketClient and IFuturesOrderManagementSocketClient for placing/canceling orders via websocket 2026-08-21 09:56:56 +02:00
Jkorf ac1e9cf2ab Added Description property to EndpointOptions classes. Updated client info string representation, added additional validation for Spot endpoints 2026-08-21 09:55:53 +02:00
Jkorf 4987e6be70 Added WithCalculatedQuantities to SharedQuantity to retrieve a copy with calculated quantities 2026-08-21 09:52:54 +02:00
Jkorf 9100c0d7f3 Added UpperPriceLimitPerecentage, LowerPriceLimitPercentage, MakerFeePercentage and TakerFeePercentage to SharedSpotSymbol model 2026-08-21 09:52:29 +02:00
Jkorf 5b1ae30e4b Updated SharedId value to be nullable 2026-08-21 09:52:00 +02:00
Jkorf 36a06e8a9a Added UpperFundingCap, LowerFundingCap to SharedFuturesSymbol model 2026-08-21 09:51:43 +02:00
Jkorf e7eb559074 Updated remaining quantity/volumes to SharedOrderQuantity model in Shared APIs, added QuantityType to SharedOrderBook, added auto calculated quote quantity where for SharedOrderQuantity where it makes sense 2026-08-21 09:51:17 +02:00
Jkorf 79e2efec70 Added IQueryResult interface to QueryResult, moved OriginalData to the base class 2026-08-21 09:48:57 +02:00
Jkorf c8c6d14329 Split UserClientProvider into base class with RestClient and derived class also containing SocketClient 2026-08-21 09:47:19 +02:00
Jkorf b8bd30a09d Added UsePublicConnectionForAuth property to SocketApiClient 2026-08-21 09:44:30 +02:00
Jkorf c2b58ab19a Removed unnecessary log from token manager 2026-08-21 09:43:57 +02:00
Jkorf 90a031c595 Added support for order polling where Shared order implementation doesn't support time filtering to UserDataTracker implementations 2026-08-17 14:17:43 +02:00
Jkorf 860d753ad6 Update README.md 2026-08-07 09:09:51 +02:00
Jkorf e16c91792c Added LBank reference 2026-08-06 10:29:02 +02:00
Jkorf a3d95da9fa Updated examples 2026-08-06 10:26:39 +02:00
JKorf 907399b878 Added ManualUpdateSubscription and UpdateSubscription additional constructor to allow producing websocket events without actual connection 2026-08-05 21:31:59 +02:00
JKorf 9eab0d967e Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-08-05 17:53:51 +02:00
JKorf 0dee68e8ae Fixed timing issue in test 2026-08-05 17:53:43 +02:00
Jkorf d838d3377f Updated logging unhandled websocket message 2026-08-05 13:13:48 +02:00
Jkorf 2141ad9061 Fixed incorrect check test output 2026-07-29 13:05:42 +02:00
Jkorf 9be8798ccf Updated to version 12.4.0 2026-07-28 12:45:36 +02:00
Jkorf 4803ed91cd Updated SharedTrade to use SharedOrderQuantity for quantities 2026-07-28 11:47:59 +02:00
Jkorf 20bddd5c37 Updated SharedFuturesTicker, SharedSpotTicker and SharedKline to use SharedOrderQuantity for volumes. Added AveragePrice property to SharedQuantity model 2026-07-28 09:05:48 +02:00
Jkorf 0e75ddb3d0 Updated json deserialization error for empty response 2026-07-24 15:42:37 +02:00
Jkorf 0e5b46002c Added DebuggerDisplay Result objects 2026-07-24 15:42:11 +02:00
Jkorf 8f7c71f9ce Add Pionex reference 2026-07-24 15:40:56 +02:00
Jkorf 73377fbb87 Updated to version 12.3.0 2026-07-23 13:47:24 +02:00
Jkorf 3a00d6371a Updated some Shared errors 2026-07-23 13:17:08 +02:00
Jkorf caf6d36bcd Extracted ConnectionCanBeUsedFor method in SocketApiClient for easier custom logic implementation 2026-07-23 13:16:59 +02:00
Jkorf e078a373da Remove duplicate warnings from testing output 2026-07-21 16:39:30 +02:00
Jkorf 007743f5a1 Added calculation of AveragePrice on Shared order models if data is available and AveragePrice is not set 2026-07-21 11:35:59 +02:00
Jkorf d06f891cee Updated to version 12.2.0 2026-07-20 14:02:03 +02:00
Jkorf 8dcbb687f5 Fixed warning 2026-07-20 13:45:32 +02:00
Jan Korf fcb36f7ee0 Shared asset and symbol types
Added SpotSymbolCatalog to Shared ISpotSymbolRestClient interface
Added FuturesSymbolCatalog to Shared IFuturesSymbolRestClient interface
Added BaseAssetType, BaseAssetSubType, QuoteAssetType and QuoteAssetSubType to GetSymbolsRequest model
Added DisplayName to SharedSpotSymbol and SharedFuturesSymbol models
Added BaseAssetType, BaseAssetSubType, QuoteAssetType and QuoteAssetSubType to SharedSpotSymbol and SharedFuturesSymbol models
Added IsStableCoin, IsCommodity and IsEquity helper methods to LibraryHelpers
2026-07-20 13:40:58 +02:00
nils2525 3cffd67518 Fixed socket individual subscription target calculation (#282)
* Fixed socket individual subscription target calculation

* Fixed socket selection when the least-loaded connection has reached its individual subscription limit
2026-07-20 09:24:01 +02:00
Jkorf ecd00ea707 Added DebuggerDisplay attributes to Shared models 2026-07-14 09:59:02 +02:00
JKorf 38a7b981ce Updated to version 12.1.1 2026-07-11 15:34:31 +02:00
JKorf c41cc3c4c7 Added timestamp deserialization support for yyyy-MM-dd HH:mm:ss.ffffff+00:00:00 2026-07-11 15:19:02 +02:00
Jkorf 4accc8039b Updated to version 12.1.0 2026-07-09 15:06:09 +02:00
Jkorf 87c86ec0c0 Fixed nullability operator on Parameters.AddCommaSeperated 2026-07-09 08:35:39 +02:00
Jkorf d9850da282 Added ExchangeParameters parameter to KlineTracker, TradeTracker and ITrackerFactory methods 2026-07-08 12:00:26 +02:00
Jkorf c4a8b02054 Updated some testing logic 2026-07-02 14:09:55 +02:00
Jkorf 34b7258496 Updated to version 12.0.2 2026-07-01 13:19:57 +02:00
Jkorf 69099922c9 Updated test validation to output a list of issues instead of throwing on the first 2026-07-01 13:16:06 +02:00
Jkorf 64c1cd5fa8 Updated RestIntegrationTest to output missing properties in a list so the running test can output them as warning instead of error 2026-07-01 09:42:55 +02:00
JKorf 936ac6640b Cleanup 2026-06-30 19:22:54 +02:00
Jkorf 68ad9ae114 Fixed warnings in tests 2026-06-30 15:25:08 +02:00
Jkorf 6238c17471 Added skills hub reference 2026-06-30 15:01:54 +02:00
Jkorf 52e6fbfe47 Updated examples 2026-06-30 12:55:20 +02:00
Jkorf 4129622d71 Added Lighter ref 2026-06-29 16:07:25 +02:00
Jkorf 95e0aefb9f Updated to version 12.0.1 2026-06-29 11:33:33 +02:00
Jkorf dd1cefdc90 Fixed bug in bool converter 2026-06-29 11:20:06 +02:00
Jkorf f4736bcf49 Updated to version 12.0.0 2026-06-29 10:55:02 +02:00
Jan Korf e823114623 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<bool> which was redundant
* Various small performance improvements
2026-06-29 10:38:09 +02:00
Jkorf afb84a1bf0 Updated to version 11.2.2 2026-06-08 14:56:14 +02:00
Jkorf 89cbd85875 Fixed timing issue causing websocket connection to possible loop in error state 2026-06-08 11:22:53 +02:00
Jkorf cc25a405c6 Updated examples 2026-06-03 10:13:34 +02:00
Jkorf d64c1171a3 Removed deprecated Protobuf separate project 2026-06-03 09:33:02 +02:00
Jkorf d4aded1ee2 Fixed Shared incorrect pagination for descending klines 2026-06-02 16:40:23 +02:00
Jkorf 271503c426 Updated to version 11.2.1 2026-06-02 11:07:10 +02:00
Jkorf f0ece589f7 Updated some test logic 2026-06-01 16:05:50 +02:00
Jkorf 3a9382ca0f Fixed test rest request checking for no-response requests 2026-05-28 16:19:38 +02:00
Jkorf 1e1a02324a Potential fix for test splitting on newline 2026-05-28 16:13:13 +02:00
Jkorf 95dd050c73 Updated to version 11.2.0 2026-05-26 10:34:26 +02:00
Jkorf 504836924c Fixed test 2026-05-26 10:30:16 +02:00
Jkorf f176fb5db5 Fixed timing issue in SymbolOrderBook stopping 2026-05-26 09:27:04 +02:00
Jkorf 6b575be1ac Improved EnumConverter initialization performance 2026-05-26 09:26:45 +02:00
Jkorf b637d5cdc4 Added RateLimitGroup option 2026-05-26 09:25:53 +02:00
JKorf a46b018c50 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-05-25 12:55:23 +02:00
JKorf 562d1d76c1 Cleanup 2026-05-25 12:55:21 +02:00
Jkorf 7dcb2241c6 Ai docs 2026-05-09 21:56:08 +02:00
Jkorf 8c4cf62d9f Fixed some examples 2026-05-07 14:15:50 +02:00
Jkorf 6e4dbcf7b1 Added AI documentation 2026-05-07 13:28:20 +02:00
Jkorf 7853834286 Added request parameter checking in RestRequestValidator 2026-04-13 11:20:41 +02:00
Jkorf 9ae1263662 Added Weex to examples 2026-04-10 12:57:37 +02:00
Jkorf ee30a6716e Added Weex reference 2026-04-10 11:54:32 +02:00
Jkorf a4b7b273dc Updated to version 11.1.1 2026-04-10 10:05:14 +02:00
Jkorf c92eeb2ec8 Added Weex client reference 2026-04-10 10:03:42 +02:00
Jkorf 4d4b0576ee Fix for rate limiter resetting 2026-04-10 10:01:13 +02:00
Jkorf 9add5e0adc Added Reset functionality to rate limiter implementation, added reset of rate limit per connection when connection is disconnected 2026-04-10 09:39:19 +02:00
Jkorf 93d92beea6 Updated to version 11.1.0 2026-04-09 10:41:33 +02:00
Jkorf a955ccbc5c Added check to EnumConverter for large enums to prevent optimistic checking 2026-04-08 14:52:26 +02:00
Jan Korf 4e2dc564dd Socket routing improvements, unit test cleanup (#276)
Updated WebSocket message routing improving performance for scenarios with multiple different subscriptions and topics
Added AddCommaSeparated helper for Enum value arrays to ParameterCollection
Improved EnumConverter performance and removed string allocation for happy path
Fixed CreateParamString extension method for ArrayParametersSerialization.Json
Fixed Shared GetOrderBookOptions and GetRecentTradeOptions base validations not being called
2026-04-08 13:04:18 +02:00
Jkorf 93034e8af8 Fixed concurrency issue when using rate limit guard for multiple gates 2026-04-07 09:57:47 +02:00
Jkorf cdd0bd83ab Added SharedRestRequestValidator for testing Shared interface implementations 2026-04-03 13:08:59 +02:00
JKorf 61d371682c Fixed CallResult returning success result in AsDataless even if Error is set 2026-03-30 19:56:58 +02:00
Jkorf 8aae769e54 Updated to version 11.0.3 2026-03-30 11:46:03 +02:00
Jkorf 1b4f6926df Fixed test concurrency issue 2026-03-30 11:42:26 +02:00
Jkorf acca3468f3 Updated Enum converter to only warn once per type for null/empty value for non-nullable enum property 2026-03-30 11:34:02 +02:00
Jkorf d6b680d42e Updated to version 11.0.2 2026-03-26 13:01:38 +01:00
JKorf 24123261e5 Fixed example 2026-03-25 21:40:40 +01:00
JKorf aab30e05f0 Updated SetOptions logic to allow calling on client without credentials 2026-03-25 21:06:54 +01:00
Jkorf 54b15b75d3 Updated examples 2026-03-24 17:25:58 +01:00
Jkorf d715c7df59 Updated to version 11.0.1 2026-03-24 14:38:39 +01:00
Jkorf e1d33f252f Fixed CreateParamString method for arrays of value types 2026-03-24 14:28:46 +01:00
Jkorf b2ba9e251f Updated to version 11.0.0 2026-03-23 14:37:34 +01:00
Jan Korf 2f9f97f4e8 Authentication update (#275)
Updated API credential logic, exchange implementation are expected to provide their own credentials implementation with ApiCredentials as base class
Removed ApiCredentials implementation used by most exchanges
Removed ApiCredentialsType Enum
Added CredentialSet base class and implementations for defining different API credentials
Added optional type param to AuthenticationProvider for the specific API credential type to improve type safety
Moved AuthenticationProvider/ApiCredentials from BaseApiClient to RestApiClient/SocketApiClient base classes
Added optional type params to RestApiClient/SocketApiClient base class to specify the AuthenticationProvider type and credentials type to improve type safety
Moved SetOptions/SetApiCredentials from BaseApiClient to RestApiClient/SocketApiClient
Extracted LibraryOptions<TRestOptions, TSocketOptions, TEnvironment> without TApiCredentials for libraries without API credentials
Removed ApiCredentials from ApiOptions, credentials can only be configured at library, rest or socket level
Added EnvironmentName to RestApiClient/SocketApiClient property
Added Unknown enum value to Shared interfaces SharedOrderStatus, SharedTransferStatus and SharedTriggerOrderStatus enums
Updated Enum converter to map value to an undefined Enum value instead of the first Enum value
Added support for checking for missing fields on RestIntegrationTest
Added BytesToHexString and HexToBytesString to ExchangeHelpers static class
Fixed bug where WebSocket connections are not reconnected when configuring Proxy with SetUpdates
Removed legacy CryptoBaseClient, CryptoRestClient and CryptoSocketClient
2026-03-23 14:28:00 +01:00
Jkorf 875696a73a added dex signing utils 2026-03-11 08:54:36 +01:00
Jkorf 90c61715d0 Added Bitstamp reference 2026-03-06 15:16:47 +01:00
Jkorf 12ca45050f Updated to version 10.8.0 2026-03-06 09:26:30 +01:00
Jkorf eaf092a334 Added ForcePathEndWithSlash setting to RequestDefinition, added encoding parameter to SetContent on REST Request, added RequestBodyContentEncoding and OmitContentTypeHeaderWithoutContent config to RestApiClient 2026-03-06 09:25:04 +01:00
Jkorf 33c0fb26a7 Updated to version 10.7.2 2026-03-02 10:53:37 +01:00
Jkorf dabbb5c868 Added small overlap in UserDataTracker polling logic to account for API endpoints not immediately having the data available 2026-03-02 10:47:25 +01:00
Jkorf 204bda8622 Updated to version 10.7.1 2026-02-25 11:00:43 +01:00
Jkorf 78e3523a4f Updated SocketConnection internal locking to fix potential deadlock 2026-02-25 09:26:12 +01:00
Jkorf 89a73747b0 Fix for test socket not working correctly with read/write socket connection lock 2026-02-24 13:15:08 +01:00
Jkorf 02b70398b3 Updated lock call SocketConnection to outside the try 2026-02-24 13:14:48 +01:00
Jkorf 73fcb47b17 Updated to version 10.7.0 2026-02-24 11:51:37 +01:00
Jkorf d41ca3459e Updated internal lock for subscription to ReaderWriterLockSlim on SocketConnection 2026-02-24 11:32:35 +01:00
Jkorf bea2b2bd7b Added Http options to Rest client options 2026-02-24 09:58:48 +01:00
Jan Korf b29cdc41f3 Shared interfaces pagination update (#274)
Updated INextPageToken parameter on Shared interfaces to PageRequest type, functionality unchanged
Added SupportsAscending and SupportsDescending properties to PaginatedEndpointOptions to expose supported data directions
Added MaxAge property to PaginatedEndpointOptions to expose the max age of data that can be requested
Added Direction property to Shared interfaces paginated requests to configure pagination data direction 
Removed PaginationSupport property from PaginatedEndpointOptions, replaced by above new properties
Updated Shared GetTradeHistoryRequest EndTime property to be optional
Updated I(Futures/Spot)OrderRestClient.GetClosed(Futures/Spot)OrdersOptions from PaginatedEndpointOptions<GetClosedOrdersRequest> to GetClosedOrdersOptions 
Updated I(Futures/Spot)OrderRestClient.Get(Futures/Spot)UserTradesOptions from PaginatedEndpointOptions<GetUserTradesRequest> to GetUserTradesOptions
Updated rate limiting PathStartFilter to ignore added or missing slash before the path
Fixed KlineTracker throwing exception if there is no data in the initial snapshot
2026-02-23 14:53:38 +01:00
JKorf 0ce2e778f4 Added check for invalid json in JsonSocketMessageHandler and virtual GetTypeIdentifierNonJson for handling non-json messages 2026-02-22 16:29:05 +01:00
JKorf 36c2411d46 Added parsing of REST response data up to 128 characters for error responses 2026-02-22 16:05:49 +01:00
Jkorf 6d3e72745a Removed check for OnlyTrackProvidedSymbols and empty initial tracking list 2026-02-17 14:54:57 +01:00
Jkorf 51c74baa26 Updated to version 10.6.2 2026-02-17 14:39:06 +01:00
Jkorf 419e01d009 Fix for websocket queries which don't expects response getting stuck in subscribing state 2026-02-17 14:35:02 +01:00
Jkorf 297eee0e1f Updated to version 10.6.1 2026-02-16 15:42:33 +01:00
Jkorf 6a9231b1e3 Fixed exception when stopping SymbolOrderBook instance when update is received while closing 2026-02-16 15:39:36 +01:00
Jkorf e40f2a15b6 Updated to version 10.6.0 2026-02-16 09:38:55 +01:00
Jkorf b94085a27a Added some code comments 2026-02-16 09:36:44 +01:00
Jkorf 5e083811df Added check SymbolOrderBook is still alive when trying to add updates to prevent unnoticed growing in the background when subscription isn't closed while book is 2026-02-16 09:33:39 +01:00
Jkorf 7dcf5cd6ea Added AddTrackedSymbolsAsync and RemoveTrackedSymbolAsync methods to UserDataTracker, updated minimal fromTime filter in UserDataTracker to 5 seconds 2026-02-16 09:11:01 +01:00
Jkorf 1471a4733f Updated symbol tracking logic on UserDataTracker, added check for startTime filter for polling being to close to current time 2026-02-13 10:12:43 +01:00
Jkorf f39d9f7cfb Updated to version 10.5.4 2026-02-12 11:43:20 +01:00
Jkorf 9fab8faa45 Fixed bug in polling time filter for UserDataTracker items 2026-02-12 11:35:04 +01:00
Jkorf 226f175343 Fixed type check ExchangeParameters GetValue 2026-02-11 14:50:39 +01:00
Jkorf 813bd9f5a1 Updated to version 10.5.3 2026-02-11 13:10:08 +01:00
Jkorf c8d2b4f09d Added check EnumConverter to detect undefined int value parsing 2026-02-11 12:57:03 +01:00
Jkorf 6560b82a3e Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders 2026-02-11 11:44:32 +01:00
JKorf e151af8f37 Updated client versions examples 2026-02-10 18:38:45 +01:00
Jkorf bdf7a07c6f Updated to version 10.5.2 2026-02-10 16:04:17 +01:00
Jkorf a8ffe90bf2 Added call to ApiClient.HandleUnhandledMessage when no websocket message processor is found based on topic to allow additional processing 2026-02-10 15:59:35 +01:00
Jkorf 3372b9eb44 Set query completed after setting Result 2026-02-10 15:40:43 +01:00
Jkorf df25221960 Combined subscribe and re-subscribe logic 2026-02-10 15:40:13 +01:00
Jkorf 7c67a014f5 Added check for subscribe queries with TimeoutBehavior.Success to complete when subscription has received update 2026-02-10 14:31:42 +01:00
Jkorf 65291b3195 Updated to version 10.5.1 2026-02-10 11:53:23 +01:00
Jkorf ece6a9d27d Fixed trading mode selection for futures listen key method in FuturesUserDataTracker 2026-02-10 11:51:19 +01:00
Jkorf 1ed29b0474 Updated to version 10.5.0 2026-02-10 10:22:05 +01:00
Jkorf be2eb01353 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-10 10:15:46 +01:00
JKorf 39cd66596d Added some tests 2026-02-09 21:49:07 +01:00
Jkorf 63f51811a9 Updated logging unmatched websocket message 2026-02-09 16:29:24 +01:00
Jkorf abda065237 Updated websocket message forwarding logic 2026-02-09 12:57:52 +01:00
Jkorf 759c8b9a58 Fixed bug in UserDataTracker orders logic incorrectly setting order to canceled status 2026-02-09 09:07:17 +01:00
JKorf 8e18482781 Added keep alive for listenkeys to UserDataTracker 2026-02-08 17:02:09 +01:00
Jkorf 87dd2d9d40 Updated to version 10.4.1 2026-02-06 17:19:08 +01:00
Jkorf f2b0cb2f0d Updated UserDataTracker to only track symbol when position size > 0, update log verbosity 2026-02-06 17:16:31 +01:00
Jkorf de0a954a91 Updated to version 10.4.0 2026-02-06 12:00:50 +01:00
Jkorf 4a79ce22ec Added timeout/ct task cleanup AsyncResetEvent 2026-02-06 11:57:56 +01:00
JKorf 40d480e1fc Added AutoTimestamp option for socket client, fixed socket client timestamp offset bug 2026-02-05 21:54:16 +01:00
Jan Korf 74e5cf6fc9 Feature/userdata tracker (#271)
Added user data tracking logic
Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
Added SharedTransferStatus Enum and property to SharedDeposit
Added PositionMode property to SharedPosition model
Added IsZero property to SharedQuantity
Renamed IWebSocket LastActionTime to LastReceiveTime
Updated CryptoExchangeWebsocketClient LastReceiveTime logic
Updated Subscription status change event handler to run sync instead of separate task
2026-02-05 16:05:13 +01:00
Jkorf 2fd3912795 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-03 08:47:26 +01:00
JKorf ec44307a0c Updated AsyncResetEvent implementation 2026-02-02 21:01:35 +01:00
Jkorf ba55705385 Added Disposed property on BaseClient and IRestClient/ISocketClient interfaces 2026-02-02 09:33:47 +01:00
Jkorf 2c63a83117 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-02 08:42:53 +01:00
JKorf 71b1e5e906 Added additional methods for requesting supported symbols to Shared ISpotSymbolRestClient/IFuturesSymbolRestClient interfaces 2026-02-01 15:02:48 +01:00
Jkorf eaeba6f27e Made Interval property of Kline tracker public 2026-01-28 10:31:44 +01:00
Jkorf 913bdaa855 Updated to version 10.3.1 2026-01-27 14:01:26 +01:00
Jkorf 5aa5790d0a Docs 2026-01-23 15:19:33 +01:00
Jkorf a8321e083e Fixed potential collection modified exception upon logging message not handled in websocket message handling 2026-01-22 09:57:02 +01:00
Jkorf ce3fa5f186 Updated to version 10.3.0 2026-01-22 09:33:47 +01:00
Jkorf fff70a9c65 Updated some testing 2026-01-21 15:38:22 +01:00
Jkorf cff33bb5ac Added handling for setting AutoTimestamp in client options when not implemented 2026-01-21 15:37:59 +01:00
Jkorf 21c8133292 Fixed HandleSubQueryResponse not getting called 2026-01-21 11:16:57 +01:00
Jkorf 76772e91ba Remove legacy websocket message handling 2026-01-21 11:14:06 +01:00
Jkorf 218e0260ce Added PlatformInfo class for specifying platform information 2026-01-21 10:14:00 +01:00
Jkorf 96b3904266 Fixed state handling for subscriptions where queries do not get a response 2026-01-20 15:19:42 +01:00
Jkorf bc8faf9822 Updated to version 10.2.5 2026-01-19 10:26:04 +01:00
JKorf 90c1b89ceb Added GetRestOffsets and GetWebsocketOffsets to TimeOffsetManager 2026-01-18 17:46:42 +01:00
JKorf 21206ffb25 Updated SymbolOrderBook.WaitUntilFirstUpdateBufferedAsync 2026-01-18 17:36:18 +01:00
JKorf 5942423bfb Updated to version 10.2.4 2026-01-17 16:29:56 +01:00
JKorf dc4abc42a7 Added WaitUntilFirstUpdateBufferedAsync method on SymbolOrderBook, fixed sequencen validation bug SymbolOrderBook 2026-01-17 16:26:25 +01:00
Jkorf c71a81e686 Added some util methods, Added CommaSplitStringConverter 2026-01-16 16:39:21 +01:00
Jkorf 550c0eabf1 Updated to version 10.2.3 2026-01-14 08:46:07 +01:00
JKorf 28a2a0c7fd Fixed semaphore exception when creating a new REST client while time sync is in progress on another client 2026-01-13 22:11:18 +01:00
JKorf 7dd1cd5bbd Added HandleUnhandledMessage virtual method to SocketApiClient to allow some processing for messages which couldn't be mapped via the normal way 2026-01-13 21:21:30 +01:00
Jkorf a7ff4416bd Updated to version 10.2.2 2026-01-13 11:18:16 +01:00
Jkorf 669d1f7c9e Allow the same websocket connection sequence number to be recorded multiple times 2026-01-13 11:16:37 +01:00
Jkorf 34ee2d3690 Updated to version 10.2.1 2026-01-13 09:29:04 +01:00
Jkorf 005fb7875d Fixed parameter URL creation for array values with ArrayParametersSerialization.MultipleValues 2026-01-13 09:25:35 +01:00
Jkorf fa9300ce97 Removed duplicate logging for rest responses in Trace verbosity 2026-01-13 09:25:07 +01:00
Jkorf fc2d3fc2d2 Updated to version 10.2.0 2026-01-12 14:30:38 +01:00
Jkorf 187ca6a4ef Fixed warning 2026-01-12 14:30:09 +01:00
Jan Korf 3b2a85d210 Feature/websocket sequencing (#267)
Added EnforceSequenceNumbers property on SocketApiClient to configure whether websocket message contain sequence numbers and if these should be checked to be sequential
Added fallback to existing websocket connection if no dedicated request connection was found
Added IntBoolConverter base class for arbitrary int value to bool mapping
Added SequenceNumber property to DataEvent object
Added _skipSequenceCheckFirstUpdateAfterSnapshotSet property for SymbolOrderBook implementations
Updated SymbolOrderBook sequenceNumber validation
Updated SymbolOrderBook log verbosities
Renamed SetInitialOrderBook to SetSnapshot in SymbolOrderBook
Renamed updateId references to sequenceNumber in SymbolOrderBook
2026-01-12 14:26:50 +01:00
Jkorf c512bee825 Updated examples 2026-01-07 14:56:18 +01:00
Jkorf 0943b052b9 Updated to version 10.1.0 2026-01-07 10:03:45 +01:00
Jan Korf a896fffdb3 Time offset management (#266)
Updated time sync / time offset management for REST API's
Added time offset tracking for WebSocket API's
Added GetAuthenticationQuery virtual method on AuthenticationProvider
Updated AuthenticationProvider GetTimestamp methods to include a one second offset by default
Added AuthenticationProvider GetTimestamp methods for SocketApiClient instances
Added ClientName property on BaseApiClient, resolving to the type name
Added ObjectOrArrayConverter JsonConverterFactory implementation for resolving json data which might be returned as object or array
Added UpdateServerTime, UpdateLocalTime and DataAge properties to (I)SymbolOrderBook
Added OutputToConsoleAsync method to (I)SymbolOrderBook
Updated SymbolOrderBook string representation
Added DataTimeLocal and DataAge properties to DataEvent object
Added SocketConnection parameter to subscription HandleSubQueryResponse and HandleUnsubQueryResponse methods
2026-01-07 10:00:14 +01:00
JKorf 177daf903b Added some utils methods 2025-12-30 09:53:30 +01:00
Jkorf aa1ebdc4ed Updated CryptoExchange.Net to version 10.0.2 2025-12-19 11:34:26 +01:00
Jkorf 38058c4a70 Updated to version 10.0.2 2025-12-19 10:14:59 +01:00
Jkorf a7eb483479 Added exception handlers for REST response processing 2025-12-19 10:09:14 +01:00
Jkorf c76931a3b4 Fixed duplicate subscription check with updated deserialization 2025-12-19 09:58:22 +01:00
Jkorf b90b7e9e0c Updated CryptoExchange.Net to 10.0.1 2025-12-18 11:13:56 +01:00
1243 changed files with 36587 additions and 127941 deletions
+92
View File
@@ -0,0 +1,92 @@
---
description: Conventions for cross-exchange C# code using CryptoExchange.Net Shared API V2 capabilities.
globs:
- "**/*.cs"
- "**/*.csproj"
alwaysApply: false
---
# CryptoExchange.Net conventions
CryptoExchange.Net is the base dependency for exchange-specific libraries. Install Binance.Net, Bybit.Net, JK.OKX.Net, or another exchange library; use `CryptoClients.Net` when the application needs the bundle.
## Use Shared API V2
For new cross-exchange code, use the fine-grained interfaces in `CryptoExchange.Net.SharedApis` and each API surface's `.SharedApi` property:
```csharp
using CryptoExchange.Net.SharedApis;
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
```
Use one capability per operation, such as `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, or `ISubscribeTradesSocket`. Do not use a broad V1 interface for new code. `.SharedClient` remains only for incremental V1 migration.
## Capability discovery
When the exchange and API surface are known, assign the typed `.SharedApi` to the required capability. When selection is dynamic, use an exchange-wide `I[Exchange]SharedApiClient` and a strongly typed reference:
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
if (match is not null)
await match.Capability.PlaceFuturesOrderAsync(request);
```
`GetCapability` returns one preferred match or `null`; `GetCapabilities` returns all matches. Specify `TradingMode` when multiple API surfaces may implement the same capability. A `SharedCapabilities` reference is a lookup key, not proof of exchange support.
## Symbols and parameters
Use `SharedSymbol` instead of native strings such as `BTCUSDT`. The exchange library performs formatting.
Capability presence does not imply support for every request property. For dynamic code, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. Supply exchange-specific values through the request's `ExchangeParameters`.
## Transports and results
- Transport-agnostic capabilities return `IExchangeCallResult<T>`.
- `...Rest` capabilities return `HttpResult<T>`.
- socket command capabilities return `QueryResult<T>`.
- `ISubscribe...Socket` capabilities return `WebSocketResult<UpdateSubscription>`.
Use `.Rest` or `.Socket` on capability references when the transport matters. Otherwise exchange-wide lookup honors `PreferredTransport`, normally REST. Always check `.Success` before `.Data` and include `.Exchange` in multi-exchange logs.
## Concurrent aggregation
```csharp
var clients = new IGetTickerRest[] { binance, okx, bybit };
var tasks = clients.Select(x =>
x.GetTickerAsync(new GetTickerRequest(symbol)));
var results = await Task.WhenAll(tasks);
```
Reuse clients through dependency injection and run independent exchange calls concurrently.
## V2 semantics
- `GetTickerAsync` and `GetAllTickersAsync` return `SharedTicker` for spot and futures.
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
- `ICloseFullPosition` closes the entire position; use an order capability for partial closes.
- Each exchange supports a subset of capabilities and parameter combinations.
## Hard rules
- Never install CryptoExchange.Net alone and expect exchange endpoints.
- Never pass exchange-native models or symbols through shared services.
- Never assume a capability or request field is supported; resolve and inspect it.
- Never use `.Result` or `.Wait()`.
- Never instantiate exchange clients per request.
- Prefer `Task.WhenAll` for independent cross-exchange calls.
- Prefer `.SharedApi` and fine-grained V2 interfaces for new code.
## Reference
- `AGENTS.md` for fuller examples
- `docs/ai-api-map.md` for the V2 interface map
- `docs/SHARED_API_V2_MIGRATION.md` for V1 migration details
- `llms-full.txt` for expanded AI context
+70
View File
@@ -0,0 +1,70 @@
# Copilot Instructions for CryptoExchange.Net
CryptoExchange.Net is the base library behind exchange-specific C#/.NET clients such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net.
## Package choice
Do not install CryptoExchange.Net alone to call an exchange. Install the exchange-specific package, or use `CryptoClients.Net` for the full bundle. For single-exchange code, prefer that library's native client. For portable multi-exchange code, use `CryptoExchange.Net.SharedApis`.
## Generate Shared API V2 code
Use fine-grained V2 capabilities through `.SharedApi`:
```csharp
using CryptoExchange.Net.SharedApis;
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
```
Choose the interface for the operation: for example, `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, or `ISubscribeTradesSocket`. V1 broad interfaces remain on `.SharedClient` for migration, but new code should use `.SharedApi`.
Use `SharedSymbol`; do not hard-code exchange-native symbol formatting in shared code.
## Runtime capability selection
When the API surface is known, assign its typed `.SharedApi` directly. When support or the API surface is selected at runtime, use the exchange-wide `I[Exchange]SharedApiClient`:
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
if (match is null)
return;
var result = await match.Capability.PlaceFuturesOrderAsync(request);
```
`GetCapability` returns one preferred `SharedCapabilityResolution<T>` or `null`; `GetCapabilities` returns all matching implementations. Include `TradingMode` when an exchange can expose multiple futures surfaces. `SharedCapabilities` entries are lookup references, not guarantees of support.
Before constructing dynamic requests, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. A capability may exist while a particular request field is unsupported.
## Results and transports
- Transport-agnostic operation: `IExchangeCallResult<T>`
- REST capability: `HttpResult<T>`
- WebSocket command capability: `QueryResult<T>`
- WebSocket subscription capability: `WebSocketResult<UpdateSubscription>`
Select `.Rest` or `.Socket` when transport-specific behavior matters. Otherwise exchange-wide selection uses `PreferredTransport`, normally REST. Always check `.Success` before `.Data`; use `.Error` and `.Exchange` for diagnostics.
## Current V2 semantics
- Ticker operations are `GetTickerAsync` and `GetAllTickersAsync`, returning `SharedTicker` for both spot and futures.
- Socket order streams use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
- `ICloseFullPosition` closes a complete position, not a partial quantity.
- Exchange support varies by operation, transport, trading mode, and request parameter.
## Engineering conventions
- Reuse clients through dependency injection.
- Use `await`; never use `.Result` or `.Wait()`.
- Use `Task.WhenAll` for independent requests across exchanges.
- Keep exchange-native models out of portable services.
- Do not infer feature support from a broad interface or request model.
See `AGENTS.md`, `docs/ai-api-map.md`, and `docs/SHARED_API_V2_MIGRATION.md` for expanded guidance.
+227
View File
@@ -0,0 +1,227 @@
---
name: cryptoexchange-net
description: Use CryptoExchange.Net Shared API V2 abstractions when generating C#/.NET code for multiple cryptocurrency exchanges, including arbitrage, routing, portfolio aggregation, exchange-agnostic bots, comparison tools, and new exchange-library implementations.
---
# CryptoExchange.Net Skill
## When to use
CryptoExchange.Net is the base library behind exchange-specific libraries such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net. Do not install it alone to call an exchange.
Choose one of these approaches:
1. One exchange: install and use that exchange's library directly.
2. Multiple exchanges: install the required exchange libraries and use `CryptoExchange.Net.SharedApis` V2 capabilities.
3. All exchanges in one package: install `CryptoClients.Net` and use its combined clients and shared capability lookup.
For new cross-exchange code, use Shared API V2. V1 aggregate interfaces remain available through `.SharedClient` for incremental migration.
## Installation
```bash
dotnet add package Binance.Net
dotnet add package JK.OKX.Net
dotnet add package Bybit.Net
```
Or install the bundle:
```bash
dotnet add package CryptoClients.Net
```
## Core pattern: fine-grained capabilities
Each exchange API surface exposes a typed `.SharedApi` aggregate. Assign it to the capability for the single operation being used:
```csharp
using Binance.Net.Clients;
using OKX.Net.Clients;
using Bybit.Net.Clients;
using CryptoExchange.Net.SharedApis;
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
IGetTickerRest bybit = new BybitRestClient().V5Api.SharedApi;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
if (!result.Success)
Console.WriteLine($"[{result.Exchange}] {result.Error}");
else
Console.WriteLine($"[{result.Exchange}] {result.Data!.LastPrice}");
```
V2 interfaces describe operations, not broad feature groups. Examples include `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, and `ISubscribeTradesSocket`. An exchange may implement one operation without implementing adjacent operations.
## Shared symbols
Use `SharedSymbol`; never pass exchange-native symbol strings to shared requests:
```csharp
var spot = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var linearPerpetual = new SharedSymbol(
TradingMode.PerpetualLinear,
"BTC",
"USDT");
```
Each exchange library converts the shared symbol to its native format. For unusual exchange asset names, configure asset aliases.
## Known surface versus runtime selection
Use the typed `.SharedApi` surface when the exchange and API are known. This is the simplest and most discoverable approach:
```csharp
IPlaceSpotOrderRest orders = restClient.SpotApi.SharedApi;
```
For runtime selection, inject the exchange-wide `I[Exchange]SharedApiClient` registered by `services.Add[Exchange](...)`. Resolve a capability with the strongly typed `SharedCapabilities` catalog:
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
if (match is null)
return; // This exchange/API/mode does not provide the operation.
var result = await match.Capability.PlaceFuturesOrderAsync(request);
```
`GetCapability` returns one `SharedCapabilityResolution<T>` containing `Capability` and `Options`, or `null` if no supported implementation matches. `GetCapabilities` returns every match on that exchange-wide client. Always specify `TradingMode` when multiple spot, linear, inverse, or delivery surfaces could match.
`SharedCapabilities` references identify interface types; they do not guarantee that an exchange implements the operation and do not send a request.
## Transport selection and result types
Some commands have three interfaces:
- Transport-agnostic, such as `IPlaceSpotOrder`, returns `IExchangeCallResult<T>`.
- REST-specific, such as `IPlaceSpotOrderRest`, returns `HttpResult<T>`.
- socket-specific, such as `IPlaceSpotOrderSocket`, returns `QueryResult<T>`.
Socket subscription capabilities such as `ISubscribeTickerSocket` return `WebSocketResult<UpdateSubscription>`.
Use `.Rest` or `.Socket` on a capability reference when transport matters. Without a transport filter, exchange-wide lookup uses `PreferredTransport`, configured through the exchange's `SharedApi.PreferredTransport` option and normally defaulting to REST.
Always check `.Success` before `.Data`. Use `.Error` for failures and the result or capability `.Exchange` value for multi-exchange logging.
## Inspect parameter support
The presence of a capability does not mean every shared request property is accepted by every exchange. Inspect its `CapabilityOptions`:
- `RequestParameterRules`: whether each shared request field is `Required`, `Optional`, or `NotSupported`.
- `ExchangeParameterRules`: required or optional exchange-specific values supplied through the request's `ExchangeParameters`.
- `SupportedTradingModes`: the trading modes supported by this implementation.
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
var leverageRule = match?.Options.RequestParameterRules
.FirstOrDefault(x => x.Name == nameof(PlaceFuturesOrderRequest.Leverage));
```
Do not infer support from the request model alone. Handle a `null` resolution and exchange-specific parameter rules before issuing dynamic trading calls.
## Common capabilities
- Market data: `IGetTickerRest`, `IGetAllTickersRest`, `IGetOrderBookRest`, `IGetKlinesRest`, `IGetRecentTradesRest`; ticker, trade, kline, and order-book socket subscriptions.
- Trading: fine-grained spot and futures place, edit, cancel, get, open-order, closed-order, and order-update capabilities.
- Account: balances, positions, user trades, fees, deposits, withdrawals, and transfers.
- Futures data: funding, open interest, leverage, mark price, and index price capabilities.
Check the typed exchange Shared API surface or use capability lookup instead of assuming universal support.
## Multi-exchange aggregation
Run independent exchange requests concurrently:
```csharp
var clients = new IGetTickerRest[] { binance, okx, bybit };
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var tasks = clients.Select(client =>
client.GetTickerAsync(new GetTickerRequest(symbol)));
var results = await Task.WhenAll(tasks);
foreach (var result in results.Where(x => x.Success))
Console.WriteLine($"{result.Exchange}: {result.Data!.LastPrice}");
```
Reuse clients through dependency injection; do not instantiate them per request.
## Dependency injection
Each exchange library provides `services.Add[Exchange](...)`. Registration includes its native clients, V1 shared interfaces, V2 operation capabilities, and exchange-wide shared client.
Inject a capability directly only when the container has one intended implementation:
```csharp
public sealed class TickerService(IGetTickerRest ticker)
{
public Task<HttpResult<SharedTicker>> GetAsync(
SharedSymbol symbol,
CancellationToken ct = default)
=> ticker.GetTickerAsync(new GetTickerRequest(symbol), ct);
}
```
If multiple exchanges or multiple API surfaces register the same capability, inject the exchange-specific shared client and select its typed API property or call `GetCapability`. Plain single-service resolution does not express which implementation you want.
For the bundle, use `services.AddCryptoClients(...)`. CryptoClients.Net provides `IExchangeSharedApiClient` for capability lookup across exchanges.
## V1 migration
V1 `.SharedClient` facades and broad interfaces such as `ISpotTickerRestClient` remain available. Migrate one operation at a time:
```csharp
// V1
await restClient.SpotApi.SharedClient.GetSpotTickerAsync(request);
// V2
await restClient.SpotApi.SharedApi.GetTickerAsync(request);
```
Important semantic changes:
- V2 ticker methods are `GetTickerAsync` and `GetAllTickersAsync` and return `SharedTicker` for both spot and futures.
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`; REST order retrieval keeps the ordinary order models.
- `ICloseFullPosition` closes the complete position. Use an order capability for partial closes where supported.
- Transport-agnostic operations return `IExchangeCallResult<T>`; select the REST or socket interface when code needs a transport-specific result.
See `docs/SHARED_API_V2_MIGRATION.md` for detailed mappings.
## Common pitfalls
- Do not install `CryptoExchange.Net` alone and expect exchange endpoints.
- Do not use `.SharedClient` for new V2 code; use `.SharedApi` and fine-grained capabilities.
- Do not mix exchange-native request or response models into cross-exchange services.
- Do not assume capability presence or request-parameter support; resolve and inspect it.
- Do not rely on preferred transport when REST or socket semantics matter.
- Do not block with `.Result` or `.Wait()`; use async calls throughout.
- Do not query exchanges sequentially when requests are independent.
## Implementing a new exchange library
- Derive API clients from `RestApiClient` and `SocketApiClient`.
- Follow the exchange credentials and options patterns used by existing libraries.
- Implement the relevant fine-grained V2 capability interfaces on typed Shared API classes.
- Publish accurate `CapabilityOptions`, including supported trading modes and request/exchange parameter rules.
- Register REST/socket Shared APIs and the exchange-wide shared client with the library's DI extension.
- Retain V1 facades only where compatibility with existing consumers is required.
Use maintained exchange libraries such as Binance.Net and Bybit.Net as implementation references.
## Reference
- Source: https://github.com/JKorf/CryptoExchange.Net
- Documentation: https://cryptoexchange.jkorf.dev/
- Shared API migration: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md
- Bundle: https://github.com/JKorf/CryptoClients.Net
- Demo app: https://github.com/JKorf/CryptoManager.Net
- Discord: https://discord.gg/MSpeEtSY8t
@@ -1,530 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using ProtoBuf;
using ProtoBuf.Meta;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
#if NET5_0_OR_GREATER
public abstract class ProtobufMessageAccessor<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
TIntermediateType> : IMessageAccessor
#else
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
#endif
{
/// <summary>
/// The intermediate deserialization object
/// </summary>
protected TIntermediateType? _intermediateType;
/// <summary>
/// Runtime type model
/// </summary>
protected RuntimeTypeModel _model;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => _intermediateType;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageAccessor(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach (var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
if (value == null)
return null;
var valueType = value.GetType();
if (valueType.IsArray)
return NodeType.Array;
if (IsSimple(valueType))
return NodeType.Value;
return NodeType.Object;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach(var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
return (T?)value;
}
/// <inheritdoc />
public T?[]? GetValues<T>(MessagePath path)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null);
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class ProtobufStreamMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
var result = _model.Deserialize(type, _stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null)
{
try
{
var result = _model.Deserialize<T>(_stream);
return new CallResult<T>(result);
}
catch(Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
IsValid = true;
return Task.FromResult(CallResult.SuccessResult);
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_intermediateType = default;
}
}
/// <summary>
/// Protobuf byte message accessor
/// </summary>
public class ProtobufByteMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
using var stream = new MemoryStream(_bytes.ToArray());
stream.Position = 0;
var result = _model.Deserialize(type, stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public override CallResult<T> Deserialize<
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(MessagePath? path = null)
#else
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
#endif
{
try
{
var result = _model.Deserialize<T>(_bytes);
return new CallResult<T>(result);
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
_intermediateType = _model.Deserialize<TIntermediate>(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_intermediateType = default;
}
}
}
@@ -1,53 +0,0 @@
using CryptoExchange.Net.Interfaces;
using ProtoBuf.Meta;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <inheritdoc />
public class ProtobufMessageSerializer : IByteMessageSerializer
{
private RuntimeTypeModel _model;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageSerializer(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public byte[] Serialize<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(T message)
#else
public byte[] Serialize<T>(T message)
#endif
{
using var memoryStream = new MemoryStream();
_model.Serialize(memoryStream, message);
return memoryStream.ToArray();
}
}
}
@@ -1,47 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>10.0.1</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion>
<FileVersion>10.0.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="10.0.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
@@ -1,128 +0,0 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>CryptoExchange.Net.Protobuf</name>
</assembly>
<members>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
<summary>
System.Text.Json message accessor
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
<summary>
The intermediate deserialization object
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
<summary>
Runtime type model
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
<summary>
System.Text.Json stream message accessor
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
<summary>
Protobuf byte message accessor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
<inheritdoc />
</member>
</members>
</doc>
-52
View File
@@ -1,52 +0,0 @@
# ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Proto
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.Protobuf.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
Protobuf support for CryptoExchange.Net.
## Release notes
* Version 10.0.1 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 10.0.0 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.13.0 - 10 Nov 2025
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.12.0 - 03 Nov 2025
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.1 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.0 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.10.0 - 15 Oct 2025
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.9.0 - 06 Oct 2025
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
* Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0
* Version 9.4.0 - 04 Aug 2025
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Updated protobuf-net package version to 3.2.56
* Version 9.3.0 - 23 Jul 2025
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.2.0 - 14 Jul 2025
* Initial release
@@ -4,6 +4,7 @@ using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -90,28 +91,25 @@ namespace CryptoExchange.Net.UnitTests
var evnt = new AsyncResetEvent(false, true); var evnt = new AsyncResetEvent(false, true);
var waiters = new List<Task<bool>>(); var waiters = new List<Task<bool>>();
for(var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
waiters.Add(evnt.WaitAsync()); waiters.Add(evnt.WaitAsync());
} }
List<bool> results = null; var remaining = waiters.ToList();
var resultsWaiter = Task.Run(async () =>
{
await Task.WhenAll(waiters);
results = waiters.Select(w => w.Result).ToList();
});
for(var i = 1; i <= 10; i++) for (var i = 0; i < 10; i++)
{ {
evnt.Set(); evnt.Set();
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion)); var completed = await Task.WhenAny(remaining);
Assert.That(await completed, Is.True);
remaining.Remove(completed);
Assert.That(remaining.Count(w => w.IsCompleted), Is.Zero);
} }
await resultsWaiter; Assert.That(remaining, Is.Empty);
Assert.That(10 == results.Count(r => r));
} }
[Test] [Test]
@@ -139,5 +137,17 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.False(result1); ClassicAssert.False(result1);
} }
[Test]
public async Task CancellingWait_Should_ReturnFalse()
{
var evnt = new AsyncResetEvent(false, true);
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
var result1 = await waiter1;
ClassicAssert.False(result1);
}
} }
} }
@@ -0,0 +1,43 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Converters.SystemTextJson;
namespace CryptoExchange.Net.UnitTests
{
internal class BodySerializationTests
{
[Test]
public void ToFormData_SerializesBasicValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = parameters.ToFormData();
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
}
[Test]
public void JsonSerializer_SerializesBasicValuesCorrectly()
{
var serializer = new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = serializer.Serialize(parameters);
Assert.That(parameterString, Is.EqualTo("{\"a\":\"1\",\"b\":2,\"c\":true}"));
}
}
}
+6 -123
View File
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -15,157 +14,41 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestBasicErrorCallResult() 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.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
} }
[Test] [Test]
public void TestBasicSuccessCallResult() public void TestBasicSuccessCallResult()
{ {
var result = new CallResult(null); var result = CallResult.Ok();
ClassicAssert.IsNull(result.Error); ClassicAssert.IsNull(result.Error);
Assert.That(result);
Assert.That(result.Success); Assert.That(result.Success);
} }
[Test] [Test]
public void TestCallResultError() public void TestCallResultError()
{ {
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown)); var result = CallResult.Fail<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError"); ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data); ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
} }
[Test] [Test]
public void TestCallResultSuccess() public void TestCallResultSuccess()
{ {
var result = new CallResult<object>(new object()); var result = CallResult.Ok<object>(new object());
ClassicAssert.IsNull(result.Error); ClassicAssert.IsNull(result.Error);
ClassicAssert.IsNotNull(result.Data); ClassicAssert.IsNotNull(result.Data);
Assert.That(result);
Assert.That(result.Success); Assert.That(result.Success);
} }
[Test]
public void TestCallResultSuccessAs()
{
var result = new CallResult<TestObjectResult>(new TestObjectResult());
var asResult = result.As<TestObject2>(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<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.As<TestObject2>(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<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(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<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(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<TestObjectResult>(
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<TestObject2>(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<TestObjectResult>(
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<TestObject2>(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 public class TestObjectResult
@@ -1,37 +1,22 @@
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests.ClientTests
{ {
[TestFixture()] [TestFixture()]
public class BaseClientTests public class BaseClientTests
{ {
[TestCase] //[TestCase]
public void DeserializingValidJson_Should_GiveSuccessfulResult() //public void DeserializingValidJson_Should_GiveSuccessfulResult()
{ //{
// arrange // // arrange
var client = new TestBaseClient(); // var client = new TestBaseClient();
// act // // act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}"); // var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// assert // // assert
Assert.That(result.Success); // Assert.That(result.Success);
} //}
[TestCase]
public void DeserializingInvalidJson_Should_GiveErrorResult()
{
// arrange
var client = new TestBaseClient();
// act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")] [TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")] [TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
@@ -0,0 +1,151 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json;
using CryptoExchange.Net.UnitTests.Implementations;
using CryptoExchange.Net.Testing;
namespace CryptoExchange.Net.UnitTests.ClientTests
{
[TestFixture()]
public class RestClientTests
{
[TestCase]
public async Task RequestingData_Should_ResultInData()
{
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
client.ApiClient1.SetNextResponse(strData, System.Net.HttpStatusCode.OK);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
public async Task ReceivingInvalidData_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"property\": 123", System.Net.HttpStatusCode.OK);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("Invalid request", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.ApiClient1.SetNextResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is DeserializeError);
Assert.That(result.Error!.Message!.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error!.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
[TestCase("POST", HttpMethodParameterPosition.InBody)]
[TestCase("POST", HttpMethodParameterPosition.InUri)]
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
{
// arrange
// act
var client = new TestRestClient();
var httpMethod = new HttpMethod(method);
client.ApiClient1.SetParameterPosition(httpMethod, pos);
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new Parameters(new ParameterSerializationSettings())
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
});
// assert
Assert.That(result.RequestMethod == new HttpMethod(method));
Assert.That(result.RequestBody?.Contains("TestParam1") == true == (pos == HttpMethodParameterPosition.InBody));
Assert.That((result.RequestUrl?.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(result.RequestBody?.Contains("TestParam2") == true == (pos == HttpMethodParameterPosition.InBody));
Assert.That((result.RequestUrl?.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
}
}
}
@@ -0,0 +1,239 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing;
using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework;
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.ClientTests
{
[TestFixture]
public class SocketClientTests
{
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
//arrange
//act
var client = new TestSocketClient(options =>
{
options.ExchangeOptions.MaxSocketConnections = 1;
});
//assert
Assert.That(1 == client.ApiClient1.ApiOptions.MaxSocketConnections);
}
[TestCase(true)]
[TestCase(false)]
public async Task ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
{
//arrange
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
socket.CanConnect = canConnect;
//act
var connectResult = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
//assert
Assert.That(connectResult.Success == canConnect);
}
[TestCase]
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
{
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
TestObject? received = null;
var resetEvent = new AsyncResetEvent(false);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
{
received = x.Data;
resetEvent.Set();
}, false, default);
socket.InvokeMessage(strData);
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
Assert.That(received != null);
}
[TestCase(false)]
[TestCase(true)]
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.ExchangeOptions.OutputOriginalData = enabled;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
string? originalData = null;
var resetEvent = new AsyncResetEvent(false);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
{
originalData = x.OriginalData;
resetEvent.Set();
}, false, default);
socket.InvokeMessage(strData);
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
// assert
Assert.That(originalData == (enabled ? strData : null));
}
[TestCase()]
public async Task UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
// act
await client.UnsubscribeAsync(result.Data!);
// assert
Assert.That(socket.Connected == false);
}
[TestCase()]
public async Task UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
var socket2 = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result2 = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
// act
await client.UnsubscribeAllAsync();
// assert
Assert.That(socket.Connected == false);
Assert.That(socket2.Connected == false);
}
[TestCase()]
public async Task BatchedSubscription_Should_NotExceedIndividualCombineTarget()
{
// arrange
var client = new TestSocketClient(options =>
{
options.SocketSubscriptionsCombineTarget = 10;
options.SocketIndividualSubscriptionCombineTarget = 10;
});
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
// act
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
// assert
Assert.That(client.ApiClient1._socketConnections.Count == 2);
Assert.That(client.ApiClient1._socketConnections.Values.All(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) <= 10));
}
[TestCase()]
public async Task BatchedSubscription_FullIndividualConnection_Should_NotPreventEligibleConnectionReuse()
{
// arrange
var client = new TestSocketClient(options =>
{
options.SocketSubscriptionsCombineTarget = 5;
options.SocketIndividualSubscriptionCombineTarget = 10;
});
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 10);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
// act
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
// assert
Assert.That(
client.ApiClient1._socketConnections.Count,
Is.EqualTo(2),
"The eligible connection should be reused instead of opening a new connection after selecting a full individual-subscription connection");
var fullConnection = client.ApiClient1._socketConnections.Values
.Single(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) == 10);
Assert.That(
fullConnection.UserSubscriptionCount,
Is.EqualTo(1),
"The full connection should not receive the normal subscription");
var eligibleConnection = client.ApiClient1._socketConnections.Values.Single(connection => connection != fullConnection);
Assert.That(
eligibleConnection.UserSubscriptionCount,
Is.EqualTo(3),
"The existing eligible connection should receive the normal subscription");
}
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
// arrange
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "ErrorWithSub" }));
var result = await subTask;
// assert
Assert.That(result.Success == false);
Assert.That(result.Error!.Message!.Contains("ErrorWithSub"));
}
[TestCase()]
public async Task SuccessResponse_Should_ConfirmSubscription()
{
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "OK" }));
var result = await subTask;
var subscription = client.ApiClient1._socketConnections.Single().Value.Subscriptions.Single();
Assert.That(subscription.Status == SubscriptionStatus.Subscribed);
}
}
}
@@ -0,0 +1,134 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class ArrayConverterTests
{
[Test()]
public void TestArrayConverter()
{
var data = new Test()
{
Prop1 = 2,
Prop2 = null,
Prop3 = "123",
Prop3Again = "123",
Prop4 = null,
Prop5 = new Test2
{
Prop21 = 3,
Prop22 = "456"
},
Prop6 = new Test3
{
Prop31 = 4,
Prop32 = "789"
},
Prop7 = TestEnum.Two,
TestInternal = new Test
{
Prop1 = 10
},
Prop8 = new Test3
{
Prop31 = 5,
Prop32 = "101"
},
};
var options = new JsonSerializerOptions()
{
TypeInfoResolver = new TestSerializerContext()
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
Assert.That(deserialized!.Prop1, Is.EqualTo(2));
Assert.That(deserialized.Prop2, Is.Null);
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
Assert.That(deserialized.Prop4, Is.Null);
Assert.That(deserialized.Prop5!.Prop21, Is.EqualTo(3));
Assert.That(deserialized.Prop5!.Prop22, Is.EqualTo("456"));
Assert.That(deserialized.Prop6!.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
Assert.That(deserialized.TestInternal!.Prop1, Is.EqualTo(10));
Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
[TestCase("[\"4.803E-5\",\"9.623e-4\",\"0.0010025\"]", 0.00004803, 0.0009623, 0.0010025)]
[TestCase("[\"81251.5\",\"1E+2\",\"-2.5E-3\"]", 81251.5, 100, -0.0025)]
[TestCase("[4.803E-5,9.623e-4,0.0010025]", 0.00004803, 0.0009623, 0.0010025)]
public void TestArrayConverterDecimalInScientificNotation(string json, decimal expected1, decimal expected2, decimal expected3)
{
// Some exchanges send small prices as strings in scientific notation, for example the
// Kucoin futures kline stream: "candles":["1789830000","4.803E-5","4.807E-5",...]
var deserialized = JsonSerializer.Deserialize<TestDecimal>(json);
Assert.That(deserialized!.Prop1, Is.EqualTo(expected1));
Assert.That(deserialized.Prop2, Is.EqualTo(expected2));
Assert.That(deserialized.Prop3, Is.EqualTo(expected3));
}
}
[JsonConverter(typeof(ArrayConverter<TestDecimal>))]
public record TestDecimal
{
[ArrayProperty(0)]
public decimal Prop1 { get; set; }
[ArrayProperty(1)]
public decimal? Prop2 { get; set; }
[ArrayProperty(2)]
public decimal Prop3 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test>))]
public record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string? Prop3 { get; set; }
[ArrayProperty(2)]
public string? Prop3Again { get; set; }
[ArrayProperty(3)]
public string? Prop4 { get; set; }
[ArrayProperty(4)]
public Test2? Prop5 { get; set; }
[ArrayProperty(5)]
public Test3? Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test? TestInternal { get; set; }
[ArrayProperty(8), JsonConversion]
public Test3? Prop8 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test2>))]
public record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string? Prop22 { get; set; }
}
public record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string? Prop32 { get; set; }
}
}
@@ -0,0 +1,68 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class BoolConverterTests
{
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", null)]
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
[TestCase(1, true)]
[TestCase(2, true)]
[TestCase(0, false)]
[TestCase(-1, false)]
public void TestBoolConverterInts(int value, bool? expected)
{
var val = $"{value}";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", false)]
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
}
public class STJBoolObject
{
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
public bool Value { get; set; }
}
}
@@ -0,0 +1,120 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class DateTimeConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
[TestCase("210512")]
[TestCase("1620777600.000")]
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output!.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600.000)]
[TestCase(1620777600000d)]
public void TestDateTimeConverterDouble(double input)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output!.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
[TestCase(1620777600000)]
[TestCase(1620777600000000)]
[TestCase(1620777600000000000)]
[TestCase(0, true)]
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output!.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600)]
[TestCase(1620777600.000)]
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
Assert.That(output!.Time == null);
}
}
public class STJTimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
}
}
@@ -0,0 +1,49 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class DecimalConverterTests
{
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("nan", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \"" + value + "\"}");
Assert.That(result!.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue : expected));
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterNumber(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result!.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
}
public class STJDecimalObject
{
[JsonConverter(typeof(DecimalConverter))]
[JsonPropertyName("test")]
public decimal? Test { get; set; }
}
}
@@ -0,0 +1,147 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class EnumConverterTests
{
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
[TestCase(null, null)]
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", (TestEnum)(-9))]
[TestCase(null, (TestEnum)(-9))]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output!.Value == expected);
}
[Test]
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
{
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
Assert.That((int)output == -99);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
Assert.That(result == expected);
}
[Test]
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
{
LibraryHelpers.StaticLogger = new TraceLogger();
var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener);
EnumConverter<TestEnum>.Reset();
try
{
Assert.Throws<Exception>(() =>
{
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
});
Assert.DoesNotThrow(() =>
{
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
});
}
finally
{
Trace.Listeners.Remove(listener);
}
}
}
public class STJEnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
public TestEnum Value { get; set; }
}
[JsonConverter(typeof(EnumConverter<TestEnum>))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
public enum TestEnum2
{
[Map("-9")]
Minus9 = -9,
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
}
@@ -0,0 +1,46 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
[TestFixture()]
public class SharedModelConversionTests
{
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored!.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored!.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
}
@@ -3,13 +3,14 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference> <PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -0,0 +1,569 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ExchangeSymbolCacheTests
{
private SharedSpotSymbol[] CreateTestSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("BTC", "EUR", "BTCEUR", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("XRP", "USDT", "XRPUSDT", false, TradingMode.Spot)
};
}
private SharedSpotSymbol[] CreateFuturesSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT-PERP", true, TradingMode.PerpetualLinear),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT-PERP", true, TradingMode.PerpetualLinear)
};
}
[Test]
public void UpdateSymbolInfo_NewTopic_Should_AddToCache()
{
// arrange
var topicId = "NewExchange";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(hasCached, Is.True);
}
[Test]
public void UpdateSymbolInfo_Should_StoreAllSymbols()
{
// arrange
var topicId = "ExchangeWithSymbols";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// assert
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]
public void UpdateSymbolInfo_CalledTwiceWithinAnHour_Should_NotUpdate()
{
// arrange
var topicId = "ExchangeNoUpdate";
var initialSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var updatedSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
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, "Env", null, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithEmptyArray_Should_CreateEmptyCache()
{
// arrange
var topicId = "EmptyExchange";
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(hasCached, Is.False);
}
[Test]
public void HasCached_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic, "Env", null);
// assert
Assert.That(result, Is.False);
}
[Test]
public void HasCached_ExistingTopicWithSymbols_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(result, Is.True);
}
[Test]
public void HasCached_ExistingTopicWithNoSymbols_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_ByName_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "LINKUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_DifferentTradingMode_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void GetSymbolsForBaseAsset_ExistingBaseAsset_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result.Any(x => x.QuoteAsset == "USDT"), Is.True);
Assert.That(result.Any(x => x.QuoteAsset == "EUR"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_CaseInsensitive_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "btc");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistingBaseAsset_Should_ReturnEmptyArray()
{
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "LINK");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistentTopic_Should_ReturnEmptyArray()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "Env", null, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void ParseSymbol_ExistingSymbol_Should_ReturnSharedSymbol()
{
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result!.BaseAsset, Is.EqualTo("BTC"));
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
}
[Test]
public void ParseSymbol_NonExistingSymbol_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NullSymbolName_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, null);
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NonExistentTopic_Should_ReturnNull()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void MultipleTopics_Should_MaintainSeparateData()
{
// arrange
var topic1 = "Exchange1";
var topic2 = "Exchange2";
var symbols1 = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var symbols2 = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, "Env", null, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, "Env", null, symbols2);
// assert
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]
public void UpdateSymbolInfo_WithDifferentTradingModes_Should_StoreCorrectly()
{
// arrange
var topicId = "ExchangeMixedModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
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, "Env", null, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, futuresSymbol), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_Should_ReturnAllTradingModes()
{
// arrange
var topicId = "ExchangeAllModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
Assert.That(result.Any(x => x.TradingMode == TradingMode.Spot), Is.True);
Assert.That(result.Any(x => x.TradingMode == TradingMode.PerpetualLinear), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithMultipleMatchingSymbols_Should_ReturnAll()
{
// arrange
var topicId = "ExchangeMultiple";
var symbols = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
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);
}
}
}
@@ -0,0 +1,20 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestAuthenticationProvider : AuthenticationProvider<TestCredentials, TestCredentials>
{
public TestAuthenticationProvider(TestCredentials credentials) : base(credentials, credentials)
{
}
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
requestConfig.Headers ??= new Dictionary<string, string>();
requestConfig.Headers["Authorization"] = Credential.Key;
}
}
}
@@ -0,0 +1,30 @@
using CryptoExchange.Net.Authentication;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestCredentials : HMACCredential
{
public TestCredentials() { }
public TestCredentials(string key, string secret) : base(key, secret)
{
}
public TestCredentials(HMACCredential credential) : base(credential.Key, credential.Secret)
{
}
public TestCredentials WithHMAC(string key, string secret)
{
if (!string.IsNullOrEmpty(Key)) throw new InvalidOperationException("Credentials already set");
Key = key;
Secret = secret;
return this;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new TestCredentials(this);
}
}
@@ -0,0 +1,64 @@
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestEnvironment : TradeEnvironment
{
public string RestClientAddress { get; }
public string SocketClientAddress { get; }
internal TestEnvironment(
string name,
string restAddress,
string streamAddress) :
base(name)
{
RestClientAddress = restAddress;
SocketClientAddress = streamAddress;
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public TestEnvironment() : base(TradeEnvironmentNames.Live)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
{ }
/// <summary>
/// Get the environment by name
/// </summary>
public static TestEnvironment? GetEnvironmentByName(string? name)
=> name switch
{
TradeEnvironmentNames.Live => Live,
"" => Live,
null => Live,
_ => default
};
/// <summary>
/// Available environment names
/// </summary>
/// <returns></returns>
public static string[] All => [Live.Name];
/// <summary>
/// Live environment
/// </summary>
public static TestEnvironment Live { get; }
= new TestEnvironment(TradeEnvironmentNames.Live,
"https://localhost",
"wss://localhost");
/// <summary>
/// Create a custom environment
/// </summary>
/// <param name="name"></param>
/// <param name="spotRestAddress"></param>
/// <param name="spotSocketStreamsAddress"></param>
/// <returns></returns>
public static TestEnvironment CreateCustom(
string name,
string spotRestAddress,
string spotSocketStreamsAddress)
=> new TestEnvironment(name, spotRestAddress, spotSocketStreamsAddress);
}
}
@@ -1,11 +1,11 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.Implementations
{ {
public class TestObject public class TestObject
{ {
[JsonPropertyName("other")] [JsonPropertyName("other")]
public string StringData { get; set; } public string StringData { get; set; } = string.Empty;
[JsonPropertyName("intData")] [JsonPropertyName("intData")]
public int IntData { get; set; } public int IntData { get; set; }
[JsonPropertyName("decimalData")] [JsonPropertyName("decimalData")]
@@ -0,0 +1,25 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestQuery : Query<TestSocketMessage>
{
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
{
MessageRouter = MessageRouter.CreateForQuery<TestSocketMessage>(request.Id.ToString(), HandleMessage);
}
private CallResult<TestSocketMessage>? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
{
if (message.Data != "OK")
return CallResult.Fail<TestSocketMessage>(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
return CallResult.Ok(message);
}
}
}
@@ -0,0 +1,66 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing.Implementations;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestApiClient : RestApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApiClient(ILoggerFactory? loggerFactory, HttpClient? httpClient, TestRestOptions options)
: base(loggerFactory, "Test", httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
{
}
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
baseAsset + quoteAsset;
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
internal void SetNextResponse(string data, HttpStatusCode code)
{
var expectedBytes = Encoding.UTF8.GetBytes(data);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new TestResponse(code, responseStream);
var request = new TestRequest(response);
var factory = new TestRequestFactory(request);
RequestFactory = factory;
}
internal async Task<HttpResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, Parameters? collection = null, RateLimitGate? rateLimitGate = null)
{
var definition = new RequestDefinition(BaseAddress, "/path", httpMethod ?? HttpMethod.Get)
{
Weight = rateLimitGate == null ? 0 : 1,
RateLimitGate = rateLimitGate
};
return await SendAsync<T>(definition, collection ?? new Parameters(new ParameterSerializationSettings()), default);
}
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
{
ParameterPositions[httpMethod] = pos;
}
}
}
@@ -0,0 +1,27 @@
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestClient : BaseRestClient<TestEnvironment, TestCredentials>
{
public TestRestApiClient ApiClient1 { get; set; }
public TestRestApiClient ApiClient2 { get; set; }
public TestRestClient(Action<TestRestOptions>? optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(HttpClient? httpClient, ILoggerFactory? loggerFactory, IOptions<TestRestOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
}
}
}
@@ -0,0 +1,33 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System.IO;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var (jsonError, jsonDocument) = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (jsonError != null)
return jsonError;
int? code = jsonDocument!.RootElement.TryGetProperty("errorCode", out var codeProp) ? codeProp.GetInt32() : null;
var msg = jsonDocument.RootElement.TryGetProperty("errorMessage", out var msgProp) ? msgProp.GetString() : null;
if (msg == null)
return new ServerError(ErrorInfo.Unknown);
if (code == null)
return new ServerError(ErrorInfo.Unknown with { Message = msg });
return new ServerError(code.Value, new ErrorInfo(ErrorType.Unknown, false, "Error") with { Message = msg });
}
}
}
@@ -0,0 +1,27 @@
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestOptions : RestExchangeOptions<TestEnvironment, TestCredentials>
{
internal static TestRestOptions Default { get; set; } = new TestRestOptions()
{
Environment = TestEnvironment.Live,
AutoTimestamp = true
};
public TestRestOptions()
{
Default?.Set(this);
}
public RestApiOptions ExchangeOptions { get; private set; } = new RestApiOptions();
internal TestRestOptions Set(TestRestOptions targetOptions)
{
targetOptions = base.Set<TestRestOptions>(targetOptions);
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
return targetOptions;
}
}
}
@@ -0,0 +1,31 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.UnitTests.Implementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests
{
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(Parameters))]
[JsonSerializable(typeof(TestObject))]
[JsonSerializable(typeof(TestSocketMessage))]
[JsonSerializable(typeof(Test))]
[JsonSerializable(typeof(Test2))]
[JsonSerializable(typeof(Test3))]
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
[JsonSerializable(typeof(STJBoolObject))]
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
[JsonSerializable(typeof(STJEnumObject))]
[JsonSerializable(typeof(STJDecimalObject))]
[JsonSerializable(typeof(STJTimeObject))]
internal partial class TestSerializerContext : JsonSerializerContext
{
}
}
@@ -0,0 +1,48 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketApiClient : SocketApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
public TestSocketApiClient(ILoggerFactory? loggerFactory, TestSocketOptions options)
: base(loggerFactory, "Test", options.Environment.SocketClientAddress, options, options.ExchangeOptions)
{
}
public TestSocketApiClient(ILoggerFactory? loggerFactory, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(loggerFactory, "Test", baseAddress, options, apiOptions)
{
}
public override ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType) => new TestSocketMessageHandler();
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
baseAsset + quoteAsset;
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct, int individualSubscriptionCount = 1)
{
var subscription = new TestSubscription<T>(_logger, handler, subQuery, false)
{
IndividualSubscriptionCount = individualSubscriptionCount
};
return await base.SubscribeAsync(subscription, ct);
}
}
}
@@ -0,0 +1,26 @@
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketClient : BaseSocketClient<TestEnvironment, TestCredentials>
{
public TestSocketApiClient ApiClient1 { get; set; }
public TestSocketApiClient ApiClient2 { get; set; }
public TestSocketClient(Action<TestSocketOptions>? optionsDelegate = null)
: this(null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestSocketClient(ILoggerFactory? loggerFactory, IOptions<TestSocketOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
}
}
}
@@ -0,0 +1,16 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal record TestSocketMessage
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
}
}
@@ -0,0 +1,33 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketMessageHandler : JsonSocketMessageHandler
{
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
public TestSocketMessageHandler()
{
}
protected override MessageTypeDefinition[] TypeEvaluators { get; } = [
new MessageTypeDefinition {
ForceIfFound = true,
Fields = [
new PropertyFieldReference("id")
],
TypeIdentifierCallback = (doc) => doc.FieldValue("id")!
},
new MessageTypeDefinition {
Fields = [
],
StaticIdentifier = "test"
},
];
}
}
@@ -0,0 +1,27 @@
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketOptions : SocketExchangeOptions<TestEnvironment, TestCredentials>
{
internal static TestSocketOptions Default { get; set; } = new TestSocketOptions()
{
Environment = TestEnvironment.Live,
AutoTimestamp = true
};
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions ExchangeOptions { get; private set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
return targetOptions;
}
}
}
@@ -0,0 +1,50 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Text;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSubscription<T> : Subscription
{
private readonly Action<DataEvent<T>> _handler;
private bool _subQuery;
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler, bool subQuery, bool authenticated) : base(logger, authenticated, true)
{
_handler = handler;
_subQuery = subQuery;
MessageRouter = MessageRouter.CreateForEvent<T>("test", HandleUpdate);
}
protected override Query? GetSubQuery(SocketConnection connection)
{
if (!_subQuery)
return null;
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false);
}
protected override Query? GetUnsubQuery(SocketConnection connection)
{
if (!_subQuery)
return null;
return new TestQuery(new TestSocketMessage { Id = 2, Data = "Unsub" }, false);
}
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
{
_handler(new DataEvent<T>("Test", data, time, originalData));
return CallResult.Ok();
}
}
}
@@ -0,0 +1,136 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class ManualUpdateSubscriptionTests
{
[Test]
public void Constructor_Should_CreateSubscribedVirtualSubscription()
{
var controller = new ManualUpdateSubscription(socketId: 12);
Assert.That(controller.Subscription.SocketId, Is.EqualTo(12));
Assert.That(controller.Subscription.Id, Is.GreaterThan(0));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Connected));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(controller.Subscription.LastReceiveTime, Is.Null);
}
[Test]
public void StateChanges_Should_BeVisibleOnSubscription()
{
var controller = new ManualUpdateSubscription();
var timestamp = new DateTime(2026, 8, 5, 12, 0, 0, DateTimeKind.Utc);
var statuses = new List<SubscriptionStatus>();
controller.Subscription.SubscriptionStatusChanged += statuses.Add;
controller.SetLastReceiveTime(timestamp);
controller.SetSocketStatus(SocketStatus.Reconnecting);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribing);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribed);
Assert.That(controller.Subscription.LastReceiveTime, Is.EqualTo(timestamp));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Reconnecting));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(statuses, Is.EqualTo(new[]
{
SubscriptionStatus.Subscribing,
SubscriptionStatus.Subscribed
}));
}
[Test]
public void LifecycleMethods_Should_InvokeSubscriptionEvents()
{
var controller = new ManualUpdateSubscription();
var error = new ServerError("Test error", ErrorInfo.Unknown);
var exception = new InvalidOperationException("Test exception");
var disconnectedPeriod = TimeSpan.FromMinutes(2);
var lost = 0;
var restored = TimeSpan.Zero;
Error? resubscribeError = null;
var paused = 0;
var unpaused = 0;
Exception? receivedException = null;
controller.Subscription.ConnectionLost += () => lost++;
controller.Subscription.ConnectionRestored += x => restored = x;
controller.Subscription.ResubscribingFailed += x => resubscribeError = x;
controller.Subscription.ActivityPaused += () => paused++;
controller.Subscription.ActivityUnpaused += () => unpaused++;
controller.Subscription.Exception += x => receivedException = x;
controller.InvokeConnectionLost();
controller.InvokeConnectionRestored(disconnectedPeriod);
controller.InvokeResubscribingFailed(error);
controller.InvokeActivityPaused();
controller.InvokeActivityUnpaused();
controller.InvokeException(exception);
Assert.That(lost, Is.EqualTo(1));
Assert.That(restored, Is.EqualTo(disconnectedPeriod));
Assert.That(resubscribeError, Is.SameAs(error));
Assert.That(paused, Is.EqualTo(1));
Assert.That(unpaused, Is.EqualTo(1));
Assert.That(receivedException, Is.SameAs(exception));
}
[Test]
public void InvokeConnectionClosed_Should_CloseAndOnlyInvokeOnce()
{
var controller = new ManualUpdateSubscription();
var closed = 0;
controller.Subscription.ConnectionClosed += () => closed++;
controller.InvokeConnectionClosed();
controller.InvokeConnectionClosed();
Assert.That(closed, Is.EqualTo(1));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Closed));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
[Test]
public async Task SubscriptionOperations_Should_InvokeCallbacks()
{
var closes = 0;
var reconnects = 0;
var resubscribes = 0;
var controller = new ManualUpdateSubscription(
closeAsync: () =>
{
closes++;
return Task.CompletedTask;
},
reconnectAsync: () =>
{
reconnects++;
return Task.CompletedTask;
},
resubscribeAsync: () =>
{
resubscribes++;
return Task.FromResult(CallResult.Ok());
});
await controller.Subscription.ReconnectAsync();
var resubscribeResult = await controller.Subscription.ResubscribeAsync();
await controller.Subscription.CloseAsync();
await controller.Subscription.CloseAsync();
Assert.That(reconnects, Is.EqualTo(1));
Assert.That(resubscribes, Is.EqualTo(1));
Assert.That(resubscribeResult.Success, Is.True);
Assert.That(closes, Is.EqualTo(1));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
}
}
+86 -97
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
@@ -10,9 +11,9 @@ namespace CryptoExchange.Net.UnitTests
public class OptionsTests public class OptionsTests
{ {
[TearDown] [TearDown]
public void Init() public void TearDown()
{ {
TestClientOptions.Default = new TestClientOptions TestRestOptions.Default = new TestRestOptions
{ {
}; };
} }
@@ -29,135 +30,123 @@ namespace CryptoExchange.Net.UnitTests
// act // act
// assert // assert
Assert.Throws(typeof(ArgumentException), Assert.Throws(typeof(ArgumentException),
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) }); () => {
var opts = new TestRestOptions()
{
ApiCredentials = new TestCredentials(key, secret)
};
opts.ApiCredentials.Validate();
});
} }
[Test] [Test]
public void TestBasicOptionsAreSet() public void TestBasicOptionsAreSet()
{ {
// arrange, act // arrange, act
var options = new TestClientOptions var options = new TestRestOptions
{ {
ApiCredentials = new ApiCredentials("123", "456"), ApiCredentials = new TestCredentials("123", "456"),
ReceiveWindow = TimeSpan.FromSeconds(10) RequestTimeout = TimeSpan.FromSeconds(10)
}; };
// assert // assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10)); Assert.That(options.RequestTimeout == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key == "123"); Assert.That(options.ApiCredentials.Key == "123");
Assert.That(options.ApiCredentials.Secret == "456"); Assert.That(options.ApiCredentials.Secret == "456");
} }
[Test] [Test]
public void TestApiOptionsAreSet() public void TestSetOptionsRest()
{ {
// arrange, act
var options = new TestClientOptions();
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
}
[Test]
public void TestClientUsesCorrectOptions()
{
var client = new TestRestClient(options => {
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
options.ApiCredentials = new ApiCredentials("333", "444");
});
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "111");
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "333");
Assert.That(authProvider2.GetSecret() == "444");
}
[Test]
public void TestClientUsesCorrectOptionsWithDefault()
{
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient(); var client = new TestRestClient();
client.SetOptions(new UpdateOptions
{
RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
});
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
Assert.That(authProvider1.GetKey() == "111"); Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(authProvider1.GetSecret() == "222"); Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
[Test] [Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault() public void TestSetOptionsRestWithCredentials()
{ {
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456"); var client = new TestRestClient();
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222"); client.SetOptions(new UpdateOptions<TestCredentials>
var client = new TestRestClient(options =>
{ {
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444"); ApiCredentials = new TestCredentials("123", "456"),
options.Environment = new TestEnvironment("Test", "https://test.test"); RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
}); });
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; Assert.That(client.ApiClient1.ApiCredentials, Is.Not.Null);
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; Assert.That(client.ApiClient1.ApiCredentials!.Key, Is.EqualTo("123"));
Assert.That(authProvider1.GetKey() == "333"); Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(authProvider1.GetSecret() == "444"); Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
Assert.That(authProvider2.GetKey() == "123"); Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
} }
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials> [Test]
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
{ {
/// <summary> TestRestOptions.Default = new TestRestOptions
/// Default options for the futures client
/// </summary>
public static TestClientOptions Default { get; set; } = new TestClientOptions()
{ {
Environment = new TestEnvironment("test", "https://test.com") ApiCredentials = new TestCredentials("111", "222"),
RequestTimeout = TimeSpan.FromSeconds(1),
}; };
/// <summary> var client1 = new TestRestClient();
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary> Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
/// The default receive window for requests Assert.That(client1.ClientOptions.ApiCredentials!.Key, Is.EqualTo("111"));
/// </summary>
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions(); TestRestOptions.Default.ApiCredentials = new TestCredentials("333", "444");
TestRestOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
public RestApiOptions Api2Options { get; set; } = new RestApiOptions(); var client2 = new TestRestClient();
internal TestClientOptions Set(TestClientOptions targetOptions) Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
{ Assert.That(client2.ClientOptions.ApiCredentials!.Key, Is.EqualTo("333"));
targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions;
} }
} }
//public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
//{
// /// <summary>
// /// Default options for the futures client
// /// </summary>
// public static TestClientOptions Default { get; set; } = new TestClientOptions()
// {
// Environment = new TestEnvironment("test", "https://test.com")
// };
// /// <summary>
// /// ctor
// /// </summary>
// public TestClientOptions()
// {
// Default?.Set(this);
// }
// /// <summary>
// /// The default receive window for requests
// /// </summary>
// public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
// public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
// public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
// internal TestClientOptions Set(TestClientOptions targetOptions)
// {
// targetOptions = base.Set<TestClientOptions>(targetOptions);
// targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
// targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
// return targetOptions;
// }
//}
} }
@@ -0,0 +1,260 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.ConverterTests;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.UnitTests
{
internal class ParameterCollectionTests
{
[Test]
public void AddingBasicValue_SetValueCorrectly()
{
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingOptionalBasicNullValue_DoesntSetValue()
{
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 Parameters(new ParameterSerializationSettings());
parameters.Add("test", 0.1m, DecimalSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingDecimalValueAsString2_SetValueCorrectly()
{
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 AddingOptionalIntNullValueAsString_DoesntSetValue()
{
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 Parameters(new ParameterSerializationSettings());
parameters.Add("test", 1L, IntegerSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongValueAsString_SetValueCorrectly()
{
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 Parameters(new ParameterSerializationSettings());
parameters.Add("test", (long?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestamp_SetValueCorrectly()
{
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 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 Parameters(new ParameterSerializationSettings());
parameters.Add("test", (DateTime?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestampString_SetValueCorrectly()
{
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 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 AddingSecondTimestamp_SetValueCorrectly()
{
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 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 AddingSecondTimestampString_SetValueCorrectly()
{
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 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 AddingEnum_SetValueCorrectly()
{
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 Parameters(new ParameterSerializationSettings());
parameters.Add("test", (TestEnum?)TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnumNullValue_DoesntSetValue()
{
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 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 Parameters(new ParameterSerializationSettings()
{
Enum = EnumSerialization.Number
});
parameters.Add("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingCommaSeparated_SetValueCorrectly()
{
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
{
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 Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingBoolString_SetValueCorrectly()
{
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 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 Parameters(new ParameterSerializationSettings());
parameters.Add("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
}
}
@@ -1,159 +1,24 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Text;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json; using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
[TestFixture()] [TestFixture()]
public class RestClientTests public class RateLimitTests
{ {
[TestCase]
public void RequestingData_Should_ResultInData()
{
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
// act
var result = client.Api1.Request<TestObject>().Result;
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
public void ReceivingInvalidData_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetResponse("{\"property\": 123", out _);
// act
var result = client.Api1.Request<TestObject>().Result;
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
// arrange
var client = new ParseErrorTestRestClient();
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api2.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
// arrange
// act
var options = new TestClientOptions();
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
options.Api1Options.OutputOriginalData = true;
options.RequestTimeout = TimeSpan.FromMinutes(1);
var client = new TestBaseClient(options);
// assert
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
[TestCase("POST", HttpMethodParameterPosition.InBody)]
[TestCase("POST", HttpMethodParameterPosition.InUri)]
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
{
// arrange
// act
var client = new TestRestClient();
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
client.SetResponse("{}", out var request);
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
},
new Dictionary<string, string>
{
{ "TestHeader", "123" }
});
// assert
Assert.That(request.Method == new HttpMethod(method));
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(request.GetHeaders().First().Key == "TestHeader");
Assert.That(request.GetHeaders().First().Value.Contains("123"));
}
[TestCase(1, 0.1)] [TestCase(1, 0.1)]
[TestCase(2, 0.1)] [TestCase(2, 0.1)]
[TestCase(5, 1)] [TestCase(5, 1)]
@@ -165,16 +30,16 @@ namespace CryptoExchange.Net.UnitTests
var triggered = false; var triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; }; 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++) 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, 1, default);
Assert.That(i == requests? triggered : !triggered); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); 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, 1, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@@ -188,14 +53,14 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); 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; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) 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, 1, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
} }
@@ -209,15 +74,15 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get); var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get);
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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, 1, default);
Assert.That(evnt == null); 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, 1, default);
Assert.That(expectLimiting ? evnt != null : evnt == null); Assert.That(expectLimiting ? evnt != null : evnt == null);
} }
@@ -232,16 +97,16 @@ namespace CryptoExchange.Net.UnitTests
bool triggered = false; bool triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; }; 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++) 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, 1, default);
Assert.That(i == requests ? triggered : !triggered); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); 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, 1, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@@ -253,14 +118,14 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); 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; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) 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, 1, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
} }
@@ -273,14 +138,14 @@ namespace CryptoExchange.Net.UnitTests
{ {
var rateLimiter = new RateLimitGate("Test"); 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)); 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; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) 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, 1, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
} }
@@ -296,15 +161,15 @@ namespace CryptoExchange.Net.UnitTests
{ {
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding)); 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 requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null }; var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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, 1, default);
Assert.That(evnt == null); 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, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -315,15 +180,15 @@ namespace CryptoExchange.Net.UnitTests
{ {
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true }; var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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, 1, default);
Assert.That(evnt == null); 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, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -335,15 +200,15 @@ namespace CryptoExchange.Net.UnitTests
{ {
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); 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 requestDefinition1 = new RequestDefinition(host1, endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true }; var requestDefinition2 = new RequestDefinition(host2, endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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, 1, default);
Assert.That(evnt == null); 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, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -355,12 +220,12 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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, 1, default);
Assert.That(evnt == null); 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, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -370,13 +235,160 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); 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 result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, 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 result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>()); Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
} }
[Test]
public async Task RateLimiterReset_Should_AllowNextRequestForSameDefinition()
{
// arrange
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("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, null, 1, RateLimitingBehaviour.Fail, null, 1, 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, 1, ct.Token);
// assert
Assert.That(evnt, Is.Null);
}
[Test]
public async Task RateLimiterReset_Should_NotAllowNextRequestForDifferentDefinition()
{
// arrange
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("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, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, 1, 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, 1, default);
// assert
Assert.That(evnt, Is.Not.Null);
}
[TestCase(null, null, true)]
[TestCase("Group1", null, false)]
[TestCase(null, "Group2", false)]
[TestCase("Group1", "Group2", false)]
[TestCase("Group3", "Group3", true)]
public async Task RateLimiterWithDifferentGroups_Should_LimitPerGroup(string? group1, string? group2, bool expectLimited)
{
// arrange
var data = JsonSerializer.Serialize(new TestObject { });
var client1 = new TestRestClient(x =>
{
x.RateLimitGroup = group1;
});
client1.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK);
var client2 = new TestRestClient(x =>
{
x.RateLimitGroup = group2;
});
client2.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK);
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(2), RateLimitWindowType.Fixed));
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act
var result1 = await client1.ApiClient1.GetResponseAsync<TestObject>(rateLimitGate: rateLimiter);
var result2 = await client2.ApiClient1.GetResponseAsync<TestObject>(rateLimitGate: rateLimiter);
// assert
Assert.That(evnt != null, Is.EqualTo(expectLimited));
}
[TestCase(RateLimitWindowType.Fixed)]
[TestCase(RateLimitWindowType.FixedAfterFirst)]
[TestCase(RateLimitWindowType.Sliding)]
[TestCase(RateLimitWindowType.Decay)]
public async Task LowerThreshold_ReservesCapacityForFullThreshold(RateLimitWindowType windowType)
{
var gate = new RateLimitGate("Test");
gate.AddGuard(new RateLimitGuard(
RateLimitGuard.PerHost,
new LimitItemTypeFilter(RateLimitItemType.Request),
10,
TimeSpan.FromHours(1),
windowType,
decayPerTimeSpan: 1));
var definition = new RequestDefinition(
"https://test.com", "/ticker", HttpMethod.Get);
var logger = new TraceLogger();
for (var i = 0; i < 8; i++)
{
var result = await gate.ProcessAsync(
logger, i, RateLimitItemType.Request, definition, null, 1,
RateLimitingBehaviour.Fail, null, 0.8, default);
Assert.That(result.Success, Is.True);
}
var marketData = await gate.ProcessAsync(
logger, 9, RateLimitItemType.Request, definition, null, 1,
RateLimitingBehaviour.Fail, null, 0.8, default);
Assert.That(marketData.Error, Is.TypeOf<ClientRateLimitError>());
var order = await gate.ProcessAsync(
logger, 10, RateLimitItemType.Request, definition, null, 2,
RateLimitingBehaviour.Fail, null, 1.0, default);
Assert.That(order.Success, Is.True);
var overHardLimit = await gate.ProcessAsync(
logger, 11, RateLimitItemType.Request, definition, null, 1,
RateLimitingBehaviour.Fail, null, 1.0, default);
Assert.That(overHardLimit.Error, Is.TypeOf<ClientRateLimitError>());
}
[TestCase(0.1, 10)]
[TestCase(1, 50)]
[TestCase(5, 250)]
[TestCase(60, 250)]
public void RateLimitSafetyMargin_DefaultIsProportionalAndCapped(double periodSeconds, int expectedMarginMilliseconds)
{
var margin = WindowTrackerHelpers.GetDefaultSafetyMargin(TimeSpan.FromSeconds(periodSeconds));
Assert.That(margin, Is.EqualTo(TimeSpan.FromMilliseconds(expectedMarginMilliseconds)));
}
[Test]
public void RateLimitGuard_ExplicitSafetyMarginIsUsed()
{
var guard = new RateLimitGuard(
RateLimitGuard.PerHost,
new LimitItemTypeFilter(RateLimitItemType.Request),
1,
TimeSpan.FromSeconds(1),
RateLimitWindowType.Sliding,
safetyMargin: TimeSpan.Zero);
Assert.That(guard.SafetyMargin, Is.EqualTo(TimeSpan.Zero));
}
} }
} }
@@ -0,0 +1,281 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
using System.Collections;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class RequestCoalescerTests
{
private static readonly TimeSpan _testTimeout = TimeSpan.FromSeconds(5);
[Test]
public async Task ExecuteAsync_WithIdenticalRequests_ShouldExecuteRequestOnce()
{
var coalescer = CreateCoalescer();
var key = CreateKey();
var requestCompletion = CreateCompletionSource<string>();
var requestStarted = CreateCompletionSource<bool>();
var requestCount = 0;
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
requestStarted.TrySetResult(true);
return await requestCompletion.Task;
}
var first = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
Assert.That(requestCount, Is.EqualTo(1));
requestCompletion.SetResult("result");
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("result"));
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
Assert.That(requestCount, Is.EqualTo(1));
}
[Test]
public async Task ExecuteAsync_WithDifferentKeys_ShouldExecuteEachRequest()
{
var coalescer = CreateCoalescer();
var firstCompletion = CreateCompletionSource<string>();
var secondCompletion = CreateCompletionSource<string>();
var firstStarted = CreateCompletionSource<bool>();
var secondStarted = CreateCompletionSource<bool>();
var requestCount = 0;
var first = coalescer.ExecuteAsync(
CreateKey(parameters: "symbol=ETH"),
async cancellationToken =>
{
Interlocked.Increment(ref requestCount);
firstStarted.TrySetResult(true);
return await firstCompletion.Task;
},
() => "canceled",
CancellationToken.None);
var second = coalescer.ExecuteAsync(
CreateKey(parameters: "symbol=BTC"),
async cancellationToken =>
{
Interlocked.Increment(ref requestCount);
secondStarted.TrySetResult(true);
return await secondCompletion.Task;
},
() => "canceled",
CancellationToken.None);
await Task.WhenAll(firstStarted.Task, secondStarted.Task).WaitAsync(_testTimeout);
Assert.That(requestCount, Is.EqualTo(2));
firstCompletion.SetResult("first");
secondCompletion.SetResult("second");
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("first"));
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("second"));
}
[Test]
public async Task ExecuteAsync_WhenOneListenerCancels_ShouldKeepSharedRequestRunning()
{
var coalescer = CreateCoalescer();
var requestCompletion = CreateCompletionSource<string>();
var requestStarted = CreateCompletionSource<CancellationToken>();
var requestCount = 0;
using var firstCancellationSource = new CancellationTokenSource();
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
requestStarted.TrySetResult(cancellationToken);
return await requestCompletion.Task;
}
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", firstCancellationSource.Token);
var requestCancellationToken = await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None);
firstCancellationSource.Cancel();
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
Assert.That(requestCancellationToken.IsCancellationRequested, Is.False);
Assert.That(requestCount, Is.EqualTo(1));
requestCompletion.SetResult("result");
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
}
[Test]
public async Task ExecuteAsync_WhenAllListenersCancel_ShouldCancelSharedRequest()
{
var coalescer = CreateCoalescer();
var requestStarted = CreateCompletionSource<bool>();
var requestCanceled = CreateCompletionSource<bool>();
using var firstCancellationSource = new CancellationTokenSource();
using var secondCancellationSource = new CancellationTokenSource();
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
using (cancellationToken.Register(() => requestCanceled.TrySetResult(true)))
{
requestStarted.TrySetResult(true);
await requestCanceled.Task;
return "request canceled";
}
}
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", firstCancellationSource.Token);
await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", secondCancellationSource.Token);
firstCancellationSource.Cancel();
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
Assert.That(requestCanceled.Task.IsCompleted, Is.False);
secondCancellationSource.Cancel();
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
Assert.That(await requestCanceled.Task.WaitAsync(_testTimeout), Is.True);
}
[Test]
public async Task ExecuteAsync_WithAlreadyCanceledListener_ShouldNotStartRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
var result = await coalescer.ExecuteAsync(
CreateKey(),
cancellationToken =>
{
Interlocked.Increment(ref requestCount);
return Task.FromResult("result");
},
() => "canceled",
cancellationSource.Token);
Assert.That(result, Is.EqualTo("canceled"));
Assert.That(requestCount, Is.Zero);
}
[Test]
public async Task ExecuteAsync_AfterRequestCompletes_ShouldStartNewRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
Task<int> ExecuteRequest(CancellationToken cancellationToken)
=> Task.FromResult(Interlocked.Increment(ref requestCount));
var first = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
var second = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
Assert.That(first, Is.EqualTo(1));
Assert.That(second, Is.EqualTo(2));
Assert.That(requestCount, Is.EqualTo(2));
}
[Test]
public void ExecuteAsync_AfterRequestFails_ShouldStartNewRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
if (Interlocked.Increment(ref requestCount) == 1)
return Task.FromException<string>(new InvalidOperationException("Request failed"));
return Task.FromResult("result");
}
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None));
Assert.That(
coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None).GetAwaiter().GetResult(),
Is.EqualTo("result"));
Assert.That(requestCount, Is.EqualTo(2));
}
[Test]
public async Task ExecuteAsync_WhenAbandonedRequestCompletes_ShouldNotRemoveReplacementRequest()
{
var coalescer = CreateCoalescer();
var key = CreateKey();
var oldRequestCompletion = CreateCompletionSource<string>();
var oldRequestStarted = CreateCompletionSource<bool>();
var replacementCompletion = CreateCompletionSource<string>();
var replacementStarted = CreateCompletionSource<bool>();
var requestCount = 0;
using var cancellationSource = new CancellationTokenSource();
async Task<string> ExecuteOldRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
oldRequestStarted.TrySetResult(true);
return await oldRequestCompletion.Task;
}
async Task<string> ExecuteReplacementRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
replacementStarted.TrySetResult(true);
return await replacementCompletion.Task;
}
var abandonedListener = coalescer.ExecuteAsync(key, ExecuteOldRequest, () => "canceled", cancellationSource.Token);
await oldRequestStarted.Task.WaitAsync(_testTimeout);
var oldPhysicalRequest = GetPhysicalRequest(coalescer, key);
cancellationSource.Cancel();
Assert.That(await abandonedListener.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
var replacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
await replacementStarted.Task.WaitAsync(_testTimeout);
oldRequestCompletion.SetResult("old result");
await oldPhysicalRequest.WaitAsync(_testTimeout);
var joinedReplacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
Assert.That(requestCount, Is.EqualTo(2));
replacementCompletion.SetResult("replacement result");
Assert.That(await replacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
Assert.That(await joinedReplacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
Assert.That(requestCount, Is.EqualTo(2));
}
private static RequestCoalescer CreateCoalescer()
=> new RequestCoalescer(NullLogger.Instance);
private static RequestCoalescingKey CreateKey(Type? responseType = null, string? parameters = null)
=> new RequestCoalescingKey("GET", "https://localhost/test", parameters, responseType ?? typeof(string));
private static TaskCompletionSource<T> CreateCompletionSource<T>()
=> new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
private static Task GetPhysicalRequest(RequestCoalescer coalescer, RequestCoalescingKey key)
{
var requestsField = typeof(RequestCoalescer).GetField("_requests", BindingFlags.Instance | BindingFlags.NonPublic)!;
var requests = (IDictionary)requestsField.GetValue(coalescer)!;
var entry = requests[key]!;
var requestProperty = entry.GetType().GetProperty("Request")!;
return ((Lazy<Task<object>>)requestProperty.GetValue(entry)!).Value;
}
}
}
@@ -0,0 +1,355 @@
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedApiClientTests
{
[Test]
public void GetCapability_ShouldUseCapabilityTradingModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear],
[TradingMode.Spot]);
var client = new TestSharedApiClient(sharedApi);
Assert.Multiple(() =>
{
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear), Is.Null);
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.Spot), Has.Count.EqualTo(1));
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.PerpetualLinear), Is.Empty);
});
}
[Test]
public void CapabilityTradingModes_ShouldDefaultToSharedApiTradingModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear],
null);
var client = new TestSharedApiClient(sharedApi);
Assert.Multiple(() =>
{
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear)?.Capability, Is.SameAs(sharedApi));
});
}
[Test]
public void GetCapabilities_WithReference_ShouldReturnMatchingCapabilities()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot],
[TradingMode.Spot]);
var client = new TestSharedApiClient(sharedApi);
var result = client.GetCapabilities(
new SharedCapabilityReference<ITestCapability>(),
TradingMode.Spot);
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Capability, Is.SameAs(sharedApi));
Assert.That(result[0].Options, Is.SameAs(sharedApi.CapabilityOptions));
}
[Test]
public void Discover_ShouldReturnAllSharedApiInformation()
{
var restApi = new TestRestSharedApi();
var socketApi = new TestSocketSharedApi();
var client = new TestDiSharedApiClient(
restApi,
socketApi);
var result = client.Discover();
Assert.Multiple(() =>
{
Assert.That(result.PreferredTransport, Is.EqualTo(SharedTransport.Socket));
Assert.That(result.SharedApis, Has.Length.EqualTo(2));
Assert.That(result.SharedApis[0].Transport, Is.EqualTo(SharedTransport.Rest));
Assert.That(result.SharedApis[1].Transport, Is.EqualTo(SharedTransport.Socket));
Assert.That(result.SharedApis[0].SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
Assert.That(result.SharedApis[0].Capabilities, Has.Length.EqualTo(1));
Assert.That(result.SharedApis[0].Authenticated, Is.False);
});
}
[Test]
public void RegisterSharedApiClient_ShouldResolvePreferredTransport()
{
var restApi = new TestRestSharedApi();
var socketApi = new TestSocketSharedApi();
var services = new ServiceCollection();
services.AddSingleton(restApi);
services.AddSingleton(socketApi);
services.RegisterSharedApiClient<
ITestSharedApiClient,
TestDiSharedApiClient>(sharedApis => sharedApis
.Add(client => client.Rest)
.Add(client => client.Socket));
using var provider = services.BuildServiceProvider();
Assert.Multiple(() =>
{
Assert.That(
provider.GetRequiredService<ITestSharedApiClient>(),
Is.Not.Null);
Assert.That(
provider.GetRequiredService<ISharedApiClientBase>(),
Is.InstanceOf<TestDiSharedApiClient>());
Assert.That(
provider.GetRequiredService<ITestCapability>(),
Is.SameAs(socketApi));
Assert.That(
provider.GetServices<ITestCapability>().ToArray(),
Is.EqualTo(new[] { socketApi }));
Assert.That(
provider.GetRequiredService<ITestRestCapability>(),
Is.SameAs(restApi));
Assert.That(
provider.GetRequiredService<ITestSocketCapability>(),
Is.SameAs(socketApi));
});
}
[Test]
public void CapabilityTradingModes_ShouldIntersectApiApplicableAndOverrideModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear, TradingMode.DeliveryLinear],
[TradingMode.Spot, TradingMode.PerpetualLinear],
[TradingMode.PerpetualLinear, TradingMode.DeliveryLinear]);
Assert.That(
sharedApi.CapabilityOptions.SupportedTradingModes,
Is.EqualTo(new[] { TradingMode.PerpetualLinear }));
}
[Test]
public void CapabilityOptions_ShouldApplyIntrinsicTradingModes()
{
var apiModes = new[]
{
TradingMode.Spot,
TradingMode.PerpetualLinear,
TradingMode.DeliveryLinear,
TradingMode.PerpetualInverse,
TradingMode.DeliveryInverse
};
var spotOptions = new PlaceSpotOrderOptions("TestExchange");
var futuresOptions = new GetPositionsOptions("TestExchange", false);
var perpetualOptions = new GetFundingRateHistoryOptions(
"TestExchange",
true,
true,
true,
100,
false);
var unrestrictedOptions = new GetTickerOptions("TestExchange");
_ = new TestOptionsHost(apiModes, spotOptions);
_ = new TestOptionsHost(apiModes, futuresOptions);
_ = new TestOptionsHost(apiModes, perpetualOptions);
_ = new TestOptionsHost(apiModes, unrestrictedOptions);
Assert.Multiple(() =>
{
Assert.That(spotOptions.SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
Assert.That(futuresOptions.SupportedTradingModes, Is.EqualTo(new[]
{
TradingMode.PerpetualLinear,
TradingMode.DeliveryLinear,
TradingMode.PerpetualInverse,
TradingMode.DeliveryInverse
}));
Assert.That(perpetualOptions.SupportedTradingModes, Is.EqualTo(new[]
{
TradingMode.PerpetualLinear,
TradingMode.PerpetualInverse
}));
Assert.That(unrestrictedOptions.SupportedTradingModes, Is.EqualTo(apiModes));
});
}
private interface ITestCapability : ISharedApiCapability
{
}
private sealed class TestApiClient : IBaseApiClient
{
public string Exchange => "TestExchange";
public string BaseAddress => "https://test.invalid";
public string FormatSymbol(
string baseAsset,
string quoteAsset,
TradingMode tradingMode,
DateTime? deliverDate = null)
=> $"{baseAsset}{quoteAsset}";
public Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation)
=> operation();
}
private interface ITestRestCapability : ITestCapability, ISharedRest
{
}
private interface ITestSocketCapability : ITestCapability, ISharedSocket
{
}
private interface ITestRestSharedApi : ITestRestCapability
{
}
private interface ITestSocketSharedApi : ITestSocketCapability
{
}
private interface ITestSharedApiClient : ISharedApiClientBase
{
ITestRestSharedApi Rest { get; }
ITestSocketSharedApi Socket { get; }
}
private sealed class TestCapabilityOptions : CapabilityOptions<SharedRequest, ITestCapability>
{
public override string Description => "Test capability";
public TestCapabilityOptions(TradingMode[]? applicableTradingModes)
: base("TestExchange", false, "TestOperation", [], applicableTradingModes)
{
}
}
private sealed class TestSharedApi : SharedApiBase, ITestCapability
{
public TestCapabilityOptions CapabilityOptions { get; }
public TestSharedApi(TradingMode[] apiTradingModes, TradingMode[]? capabilityTradingModes)
: this(apiTradingModes, capabilityTradingModes, null)
{
}
public TestSharedApi(
TradingMode[] apiTradingModes,
TradingMode[]? capabilityTradingModes,
TradingMode[]? applicableTradingModes)
: base(
SharedTransport.Rest,
new TestApiClient(),
apiTradingModes,
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
var options = new TestCapabilityOptions(applicableTradingModes)
{
SupportedTradingModeOverrides = capabilityTradingModes
};
SetCapabilities(options);
CapabilityOptions = options;
}
public override SharedClientInfo Discover() => new();
}
private sealed class TestOptionsHost : SharedApiBase
{
public TestOptionsHost(TradingMode[] apiTradingModes, CapabilityOptions capabilityOptions)
: base(
SharedTransport.Rest,
new TestApiClient(),
apiTradingModes,
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
SetCapabilities(capabilityOptions);
}
public override SharedClientInfo Discover() => new();
}
private abstract class TestTransportSharedApi : SharedApiBase
{
protected TestTransportSharedApi(SharedTransport transport)
: base(
transport,
new TestApiClient(),
[TradingMode.Spot],
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
SetCapabilities(new TestCapabilityOptions([TradingMode.Spot]));
}
public override SharedClientInfo Discover() => new()
{
Exchange = this.Exchange,
TypeName = GetType().Name,
SupportedTradingModes = this.SupportedTradingModes,
Transport = this.Transport,
Authenticated = this.Authenticated,
Capabilities = ((ISharedApi)this).Capabilities.ToArray()
};
}
private sealed class TestRestSharedApi : TestTransportSharedApi, ITestRestSharedApi
{
public TestRestSharedApi() : base(SharedTransport.Rest)
{
}
}
private sealed class TestSocketSharedApi : TestTransportSharedApi, ITestSocketSharedApi
{
public TestSocketSharedApi() : base(SharedTransport.Socket)
{
}
}
private sealed class TestDiSharedApiClient : SharedApiClientBase, ITestSharedApiClient
{
public ITestRestSharedApi Rest { get; }
public ITestSocketSharedApi Socket { get; }
public TestDiSharedApiClient(
TestRestSharedApi rest,
TestSocketSharedApi socket)
: base(SharedTransport.Socket, rest, socket)
{
Rest = rest;
Socket = socket;
}
}
private sealed class TestSharedApiClient : SharedApiClientBase
{
public TestSharedApiClient(params ISharedApiCapability[] sharedApis)
: base(SharedTransport.Rest, sharedApis)
{
}
}
}
}
@@ -0,0 +1,89 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedCapabilitiesTests
{
[Test]
public void AllSharedApiCapabilities_ShouldBeListedInSharedCapabilities()
{
var markerTypes = new[]
{
typeof(ISharedApiCapability),
typeof(ISharedRest),
typeof(ISharedSocket),
typeof(ISharedSubscription)
};
var expectedCapabilities = typeof(ISharedApiCapability).Assembly
.GetTypes()
.Where(x => x.IsInterface
&& typeof(ISharedApiCapability).IsAssignableFrom(x)
&& !markerTypes.Contains(x))
.OrderBy(x => x.FullName)
.ToArray();
var listedCapabilities = GetTypeAndNestedTypes(typeof(SharedCapabilities))
.SelectMany(x => x.GetProperties(
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly))
.SelectMany(x => x.PropertyType.GetGenericArguments())
.Where(x => x.IsInterface
&& typeof(ISharedApiCapability).IsAssignableFrom(x))
.ToArray();
var missingCapabilities = expectedCapabilities
.Except(listedCapabilities)
.Select(x => x.Name)
.ToArray();
var unknownCapabilities = listedCapabilities
.Except(expectedCapabilities)
.Select(x => x.Name)
.ToArray();
var duplicateCapabilities = listedCapabilities
.GroupBy(x => x)
.Where(x => x.Count() > 1)
.Select(x => x.Key.Name)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(
missingCapabilities,
Is.Empty,
$"Capabilities missing from SharedCapabilities: " +
string.Join(", ", missingCapabilities));
Assert.That(
unknownCapabilities,
Is.Empty,
$"Unknown capabilities listed in SharedCapabilities: " +
string.Join(", ", unknownCapabilities));
Assert.That(
duplicateCapabilities,
Is.Empty,
$"Capabilities listed multiple times in SharedCapabilities: " +
string.Join(", ", duplicateCapabilities));
});
}
private static IEnumerable<Type> GetTypeAndNestedTypes(Type type)
{
yield return type;
foreach (var nestedType in type.GetNestedTypes(BindingFlags.Public))
{
foreach (var result in GetTypeAndNestedTypes(nestedType))
yield return result;
}
}
}
}
@@ -0,0 +1,180 @@
using NUnit.Framework;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedParametersTests
{
[Test]
public void AllSharedAPIOptions_ShouldListAllDefaultParametersInRequestParameterRules()
{
var failures = new List<string>();
var optionsTypes = typeof(CapabilityOptions).Assembly
.GetTypes()
.Where(x => x.IsClass
&& !x.IsAbstract
&& typeof(CapabilityOptions).IsAssignableFrom(x))
.OrderBy(x => x.FullName)
.ToArray();
foreach (var optionsType in optionsTypes)
{
var genericOptionsType = GetGenericOptionsType(optionsType);
if (genericOptionsType == null)
{
if (optionsType == typeof(ClosePositionOptions))
continue;
failures.Add($"{optionsType.Name}: unable to determine request type");
continue;
}
var requestType = genericOptionsType.GetGenericArguments()[0];
var defaultRulesField = GetDefaultParameterRulesField(optionsType);
if (defaultRulesField == null)
{
failures.Add($"{optionsType.Name}: no _defaultParameterRules field found");
continue;
}
var rules = (RequestParameterDescription[]?)defaultRulesField.GetValue(null);
if (rules == null)
{
failures.Add($"{optionsType.Name}: _defaultParameterRules is null");
continue;
}
var constructorParameterNames = requestType
.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
.SelectMany(x => x.GetParameters())
.Where(x => !string.Equals(
x.Name,
"exchangeParameters",
StringComparison.OrdinalIgnoreCase))
.Select(x => x.Name!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var parameterProperties = requestType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.DeclaringType == requestType
|| constructorParameterNames.Contains(x.Name))
.Where(x => x.Name != nameof(SharedRequest.ExchangeParameters))
.Select(x => x.Name)
.OrderBy(x => x)
.ToArray();
var ruleNames = rules
.Select(x => x.Name)
.ToHashSet(StringComparer.Ordinal);
foreach (var parameterProperty in parameterProperties)
{
if (!ruleNames.Contains(parameterProperty))
{
failures.Add(
$"{optionsType.Name}: request property " +
$"{requestType.Name}.{parameterProperty} has no default request parameter rule");
}
}
}
Assert.That(
failures,
Is.Empty,
$"Missing default request parameter rules:{Environment.NewLine}" +
string.Join(Environment.NewLine, failures));
}
[Test]
public void RequiredExchangeParameter_ShouldAcceptNameOrAnyAlias()
{
const string exchange = "TestExchange";
var options = new GetTickerOptions(exchange)
{
ExchangeParameterRules =
[
ExchangeParameterRule.Required(
"Parameter",
"Test parameter",
1,
"Alias1",
"Alias2")
]
};
var missingResult = options.ValidateRequest(null, null, [TradingMode.Spot]);
var nameResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Parameter", 1)),
null,
[TradingMode.Spot]);
var firstAliasResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Alias1", 1)),
null,
[TradingMode.Spot]);
var secondAliasResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Alias2", 1)),
null,
[TradingMode.Spot]);
Assert.Multiple(() =>
{
Assert.That(missingResult, Is.Not.Null);
Assert.That(nameResult, Is.Null);
Assert.That(firstAliasResult, Is.Null);
Assert.That(secondAliasResult, Is.Null);
});
}
[Test]
public void RequiredExchangeParameterWithoutAliases_ShouldBeRequired()
{
const string exchange = "TestExchange";
var options = new GetTickerOptions(exchange)
{
ExchangeParameterRules =
[
ExchangeParameterRule.Required(
"Parameter",
"Test parameter",
1)
]
};
var result = options.ValidateRequest(null, null, [TradingMode.Spot]);
Assert.That(result, Is.Not.Null);
}
private static Type? GetGenericOptionsType(Type optionsType)
{
for (var type = optionsType; type != null; type = type.BaseType)
{
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(CapabilityOptions<,>))
return type;
}
return null;
}
private static FieldInfo? GetDefaultParameterRulesField(Type optionsType)
{
for (var type = optionsType; type != null; type = type.BaseType)
{
var field = type.GetField(
"_defaultParameterRules",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
if (field != null)
return field;
}
return null;
}
}
}
@@ -0,0 +1,632 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedQuantityTests
{
[Test]
public void SharedQuantityReference_IsZero_AllNull_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_AllZero_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(0, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_BaseAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(1.5m, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_QuoteAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, 100m, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_ContractsSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, 10m);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_NegativeValue_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(-1m, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_Should_SetBaseAssetQuantity()
{
// arrange
var expectedQuantity = 1.5m;
// act
var quantity = SharedQuantity.Base(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Base_WithZero_Should_SetZeroQuantity()
{
// arrange & act
var quantity = SharedQuantity.Base(0m);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0m));
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_WithLargeValue_Should_SetCorrectly()
{
// arrange
var largeValue = 999999.123456789m;
// act
var quantity = SharedQuantity.Base(largeValue);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(largeValue));
}
[Test]
public void SharedQuantity_Quote_Should_SetQuoteAssetQuantity()
{
// arrange
var expectedQuantity = 100m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Quote_WithDecimal_Should_PreserveDecimals()
{
// arrange
var expectedQuantity = 50.123456m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_Should_SetContractQuantity()
{
// arrange
var expectedQuantity = 10m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_WithFractionalValue_Should_SetCorrectly()
{
// arrange
var expectedQuantity = 2.5m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_BaseFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 50m;
var expectedBase = 2m; // 100 / 50 = 2
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedBase));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_BaseFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 100m;
var price = 7m;
var lotSize = 0.1m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.1 = 14.3
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.3m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithHighPrecision_Should_HandleCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var price = 0.00001m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 2m;
var price = 50m;
var expectedQuote = 100m; // 2 * 50 = 100
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuote));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_QuoteFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
// 1.234567 * 10 = 12.34567 should round to 12.35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(12.35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 3.456m;
var price = 10m;
var lotSize = 1m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, 8, lotSize);
// assert
// 3.456 * 10 = 34.56 should adjust to nearest 1 = 35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithSmallValues_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 0.001m;
var price = 0.1m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0.0001m));
}
[Test]
public void SharedQuantity_ContractsFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 10m;
var expectedContracts = 10m; // 100 / 10 = 10
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 100m;
var contractSize = 7m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.5 = 14.5
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.5m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithFractionalContract_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 1m;
var contractSize = 0.1m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(10m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
var expectedContracts = 2m; // 1000 / 50 / 10 = 2
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var contractSize = 3m;
var price = 7m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 100 / 7 / 3 = 4.761904... should round to 4.76
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(4.76m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 7m;
var price = 13m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, 8, lotSize);
// assert
// 1000 / 13 / 7 = 10.989... should adjust to nearest 0.5 = 11.0
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(11.0m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithComplexValues_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 5000m;
var contractSize = 0.01m;
var price = 25000m;
var decimalPlaces = 4;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 5000 / 25000 / 0.01 = 20
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(20m));
}
[Test]
public void SharedOrderQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedOrderQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetBaseAsset()
{
// arrange
var baseAsset = 5m;
// act
var quantity = new SharedOrderQuantity(baseAssetQuantity: baseAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetQuoteAsset()
{
// arrange
var quoteAsset = 100m;
// act
var quantity = new SharedOrderQuantity(quoteAssetQuantity: quoteAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetContracts()
{
// arrange
var contracts = 10m;
// act
var quantity = new SharedOrderQuantity(contractQuantity: contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetAllValues()
{
// arrange
var baseAsset = 1m;
var quoteAsset = 50m;
var contracts = 5m;
// act
var quantity = new SharedOrderQuantity(baseAsset, quoteAsset, contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_WithNullValues_Should_HandleCorrectly()
{
// arrange & act
var quantity = new SharedOrderQuantity(null, null, null);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(10m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentValues_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(20m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentTypes_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Quote(10m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedOrderQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = new SharedOrderQuantity(5m, 100m, 2m);
var quantity2 = new SharedOrderQuantity(5m, 100m, 2m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
// Default decimalPlaces = 8, default lotSize = 0.00000001
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
}
}
@@ -0,0 +1,400 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedSymbolTests
{
[Test]
public void SharedSymbol_Constructor_Should_SetAllProperties()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.Null);
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithDeliveryTime_Should_SetDeliveryTime()
{
// arrange
var tradingMode = TradingMode.DeliveryLinear;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var deliveryTime = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc);
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliveryTime);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.EqualTo(deliveryTime));
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithNullDeliveryTime_Should_SetToNull()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "ETH";
var quoteAsset = "BTC";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime: null);
// assert
Assert.That(symbol.DeliverTime, Is.Null);
}
[TestCase(TradingMode.Spot)]
[TestCase(TradingMode.PerpetualLinear)]
[TestCase(TradingMode.PerpetualInverse)]
[TestCase(TradingMode.DeliveryLinear)]
[TestCase(TradingMode.DeliveryInverse)]
public void SharedSymbol_Constructor_WithDifferentTradingModes_Should_SetCorrectly(TradingMode tradingMode)
{
// arrange
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_Should_SetSymbolName()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "BTC-USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
Assert.That(symbol.DeliverTime, Is.Null);
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithCustomFormat_Should_SetCorrectly()
{
// arrange
var tradingMode = TradingMode.PerpetualLinear;
var baseAsset = "ETH";
var quoteAsset = "USDT";
var symbolName = "ETHUSDT-PERP";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithEmptyString_Should_SetEmptyString()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(""));
}
[Test]
public void GetSymbol_WithSymbolNameSet_Should_ReturnSymbolName()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "CUSTOM-BTC-USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("CUSTOM-BTC-USDT"));
}
[Test]
public void GetSymbol_WithSymbolNameNull_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}/{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC/USDT"));
}
[Test]
public void GetSymbol_WithComplexFormatFunction_Should_ApplyCorrectly()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => t == TradingMode.PerpetualLinear ? $"{b}{q}-PERP" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("ETHUSDT-PERP"));
}
[Test]
public void GetSymbol_WithDeliveryTime_Should_PassDeliveryTimeToFormatter()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime);
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => d.HasValue ? $"{b}{q}_{d.Value:yyyyMMdd}" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDT_20260625"));
}
[Test]
public void GetSymbol_WithTradingMode_Should_PassTradingModeToFormatter()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualInverse, "BTC", "USD");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) =>
{
return t switch
{
TradingMode.Spot => $"{b}{q}",
TradingMode.PerpetualLinear => $"{b}{q}-PERP",
TradingMode.PerpetualInverse => $"{b}{q}I-PERP",
_ => $"{b}{q}"
};
});
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDI-PERP"));
}
[Test]
public void GetSymbol_WithEmptySymbolName_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC-USDT"));
}
[Test]
public void GetSymbol_WithWhitespaceSymbolName_Should_ReturnWhitespace()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", " ");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo(" "));
}
[Test]
public void SharedSymbol_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentBaseAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentQuoteAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "EUR");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentTradingMode_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentDeliveryTime_Should_NotBeEqual()
{
// arrange
var deliveryTime1 = new DateTime(2026, 6, 25);
var deliveryTime2 = new DateTime(2026, 9, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime1);
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime2);
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentSymbolName_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTC-USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_OneWithSymbolNameOneWithout_Should_NotBeEqual()
{
// NOTE; although this should probably be equal it's considered not because the SymbolName property isn't equal
// Overridding equality to ignore SymbolName would be possible but would break the default record equality behavior and cause confusion
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_WithAllPropertiesSet_Should_BeEqual()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_Properties_Should_BeSettable()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
symbol.BaseAsset = "ETH";
symbol.QuoteAsset = "EUR";
symbol.TradingMode = TradingMode.PerpetualLinear;
symbol.SymbolName = "CUSTOM";
symbol.DeliverTime = DateTime.UtcNow;
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo("ETH"));
Assert.That(symbol.QuoteAsset, Is.EqualTo("EUR"));
Assert.That(symbol.TradingMode, Is.EqualTo(TradingMode.PerpetualLinear));
Assert.That(symbol.SymbolName, Is.EqualTo("CUSTOM"));
Assert.That(symbol.DeliverTime, Is.Not.Null);
}
[Test]
public void SharedSymbol_WithSpecialCharactersInAssets_Should_HandleCorrectly()
{
// arrange
var baseAsset = "BTC-123";
var quoteAsset = "USDT_2.0";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
[Test]
public void SharedSymbol_WithLongAssetNames_Should_HandleCorrectly()
{
// arrange
var baseAsset = "VERYLONGASSETNAMEFORTESTING";
var quoteAsset = "ANOTHERVERYLONGASSETNAME";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
}
}
@@ -1,234 +0,0 @@
//using CryptoExchange.Net.Objects;
//using CryptoExchange.Net.Objects.Sockets;
//using CryptoExchange.Net.Sockets;
//using CryptoExchange.Net.Testing.Implementations;
//using CryptoExchange.Net.UnitTests.TestImplementations;
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
//using Microsoft.Extensions.Logging;
//using Moq;
//using NUnit.Framework;
//using NUnit.Framework.Legacy;
//using System;
//using System.Collections.Generic;
//using System.Net.Sockets;
//using System.Text.Json;
//using System.Threading;
//using System.Threading.Tasks;
//namespace CryptoExchange.Net.UnitTests
//{
// [TestFixture]
// public class SocketClientTests
// {
// [TestCase]
// public void SettingOptions_Should_ResultInOptionsSet()
// {
// //arrange
// //act
// var client = new TestSocketClient(options =>
// {
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
// options.SubOptions.MaxSocketConnections = 1;
// });
// //assert
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
// }
// [TestCase(true)]
// [TestCase(false)]
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
// {
// //arrange
// var client = new TestSocketClient();
// var socket = client.CreateSocket();
// socket.CanConnect = canConnect;
// //act
// var connectResult = client.SubClient.ConnectSocketSub(
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// //assert
// Assert.That(connectResult.Success == canConnect);
// }
// [TestCase]
// public void SocketMessages_Should_BeProcessedInDataHandlers()
// {
// // arrange
// var client = new TestSocketClient(options => {
// options.ReconnectInterval = TimeSpan.Zero;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var rstEvent = new ManualResetEvent(false);
// Dictionary<string, string> result = null;
// client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// result = messageEvent.Data;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// // act
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
// rstEvent.WaitOne(1000);
// // assert
// Assert.That(result["property"] == "123");
// }
// [TestCase(false)]
// [TestCase(true)]
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
// {
// // arrange
// var client = new TestSocketClient(options =>
// {
// options.ReconnectInterval = TimeSpan.Zero;
// options.SubOptions.OutputOriginalData = enabled;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var rstEvent = new ManualResetEvent(false);
// string original = null;
// client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// original = messageEvent.OriginalData;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// // act
// socket.InvokeMessage(msgToSend);
// rstEvent.WaitOne(1000);
// // assert
// Assert.That(original == (enabled ? msgToSend : null));
// }
// [TestCase()]
// public void UnsubscribingStream_Should_CloseTheSocket()
// {
// // arrange
// var client = new TestSocketClient(options =>
// {
// options.ReconnectInterval = TimeSpan.Zero;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// client.SubClient.ConnectSocketSub(sub);
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var ups = new UpdateSubscription(sub, subscription);
// sub.AddSubscription(subscription);
// // act
// client.UnsubscribeAsync(ups).Wait();
// // assert
// Assert.That(socket.Connected == false);
// }
// [TestCase()]
// public void UnsubscribingAll_Should_CloseAllSockets()
// {
// // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket1 = client.CreateSocket();
// var socket2 = client.CreateSocket();
// socket1.CanConnect = true;
// socket2.CanConnect = true;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// client.SubClient.ConnectSocketSub(sub1);
// client.SubClient.ConnectSocketSub(sub2);
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// sub1.AddSubscription(subscription1);
// sub2.AddSubscription(subscription2);
// var ups1 = new UpdateSubscription(sub1, subscription1);
// var ups2 = new UpdateSubscription(sub2, subscription2);
// // act
// client.UnsubscribeAllAsync().Wait();
// // assert
// Assert.That(socket1.Connected == false);
// Assert.That(socket2.Connected == false);
// }
// [TestCase()]
// public void FailingToConnectSocket_Should_ReturnError()
// {
// // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket = client.CreateSocket();
// socket.CanConnect = false;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// // act
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
// // assert
// ClassicAssert.IsFalse(connectResult.Success);
// }
// [TestCase()]
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
// {
// // arrange
// var channel = "trade_btcusd";
// var client = new TestSocketClient(opt =>
// {
// opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
// await sub;
// // assert
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
// }
// [TestCase()]
// public async Task SuccessResponse_Should_ConfirmSubscription()
// {
// // arrange
// var channel = "trade_btcusd";
// var client = new TestSocketClient(opt =>
// {
// opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
// await sub;
// // assert
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
// }
// }
//}
@@ -0,0 +1,235 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class QueryRouterTests
{
[Test]
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
{
// arrange
var routes = new MessageRoute[]
{
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
};
var router = new QueryRouter(routes);
// act
var type1Routes = router.GetRoutes("type1");
var type2Routes = router.GetRoutes("type2");
var missingRoutes = router.GetRoutes("missing");
// assert
Assert.That(type1Routes, Is.Not.Null);
Assert.That(type2Routes, Is.Not.Null);
Assert.That(missingRoutes, Is.Null);
Assert.That(type1Routes, Is.TypeOf<QueryRouteCollection>());
Assert.That(type2Routes, Is.TypeOf<QueryRouteCollection>());
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
}
[Test]
public void AddRoute_Should_SetMultipleReaders_WhenAnyRouteAllowsMultipleReaders()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
// act
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) => null));
var beforeMultipleReaders = collection.MultipleReaders;
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null, true));
var afterMultipleReaders = collection.MultipleReaders;
// assert
Assert.That(beforeMultipleReaders, Is.False);
Assert.That(afterMultipleReaders, Is.True);
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
[Test]
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
[Test]
public void Handle_Should_StopAfterFirstNonNullMatchingResult_WhenMultipleReadersIsFalse()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return CallResult.Ok();
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first" }));
}
[Test]
public void Handle_Should_ContinueAfterNonNullMatchingResult_WhenMultipleReadersIsTrue()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}, true));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return CallResult.Ok();
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_ContinueUntilNonNullResult_WhenEarlierMatchingRoutesReturnNull()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return null;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("third");
return CallResult.Ok();
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_ReturnHandledTrue_WhenMatchingRoutesReturnNull()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
}
}
}
@@ -0,0 +1,168 @@
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.Sockets.Interfaces;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class RoutingTableTests
{
[Test]
public void Update_Should_CreateEntriesPerTypeIdentifier_WithCorrectDeserializationTypeAndHandlers()
{
// arrange
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)));
var table = new RoutingTable();
// act
table.Update(new IMessageProcessor[] { processor1, processor2 });
var type1Entry = table.GetRouteTableEntry("type1");
var type2Entry = table.GetRouteTableEntry("type2");
var missingEntry = table.GetRouteTableEntry("missing");
// assert
Assert.That(type1Entry, Is.Not.Null);
Assert.That(type2Entry, Is.Not.Null);
Assert.That(missingEntry, Is.Null);
Assert.That(type1Entry!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type1Entry.IsStringOutput, Is.True);
Assert.That(type1Entry.Handlers, Has.Count.EqualTo(1));
Assert.That(type1Entry.Handlers.Single(), Is.SameAs(processor1));
Assert.That(type2Entry!.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(type2Entry.IsStringOutput, Is.False);
Assert.That(type2Entry.Handlers, Has.Count.EqualTo(1));
Assert.That(type2Entry.Handlers.Single(), Is.SameAs(processor2));
}
[Test]
public void Update_Should_AddMultipleProcessors_ForSameTypeIdentifier()
{
// arrange
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
var table = new RoutingTable();
// act
table.Update(new IMessageProcessor[] { processor1, processor2 });
var entry = table.GetRouteTableEntry("type1");
// assert
Assert.That(entry, Is.Not.Null);
Assert.That(entry!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(entry.Handlers, Has.Count.EqualTo(2));
Assert.That(entry.Handlers, Does.Contain(processor1));
Assert.That(entry.Handlers, Does.Contain(processor2));
}
[Test]
public void Update_Should_ReplacePreviousEntries()
{
// arrange
var initialProcessor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var replacementProcessor = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute.CreateForEvent<int>("type2", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { initialProcessor });
// act
table.Update(new IMessageProcessor[] { replacementProcessor });
var oldEntry = table.GetRouteTableEntry("type1");
var newEntry = table.GetRouteTableEntry("type2");
// assert
Assert.That(oldEntry, Is.Null);
Assert.That(newEntry, Is.Not.Null);
Assert.That(newEntry!.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(newEntry.Handlers, Has.Count.EqualTo(1));
Assert.That(newEntry.Handlers.Single(), Is.SameAs(replacementProcessor));
}
[Test]
public void Update_WithEmptyProcessors_Should_ClearEntries()
{
// arrange
var processor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { processor });
// act
table.Update(Array.Empty<IMessageProcessor>());
// assert
Assert.That(table.GetRouteTableEntry("type1"), Is.Null);
}
[Test]
public void TypeRoutingCollection_Should_SetIsStringOutput_BasedOnDeserializationType()
{
// arrange & act
var stringCollection = new TypeRoutingCollection(typeof(string));
var intCollection = new TypeRoutingCollection(typeof(int));
// assert
Assert.That(stringCollection.IsStringOutput, Is.True);
Assert.That(stringCollection.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(stringCollection.Handlers, Is.Empty);
Assert.That(intCollection.IsStringOutput, Is.False);
Assert.That(intCollection.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(intCollection.Handlers, Is.Empty);
}
private sealed class TestMessageProcessor : IMessageProcessor
{
public int Id { get; }
public MessageRouter MessageRouter { get; }
public TestMessageProcessor(int id, MessageRouter messageRouter)
{
Id = id;
MessageRouter = messageRouter;
}
#pragma warning disable CS0067 // The event is never used, but it's required by the interface
public event Action? OnMessageRouterUpdated;
#pragma warning restore CS0067
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result)
{
return true;
}
}
}
}
@@ -0,0 +1,160 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default.Routing;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class SubscriptionRouterTests
{
[Test]
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
{
// arrange
var routes = new MessageRoute[]
{
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
};
var router = new SubscriptionRouter(routes);
// act
var type1Routes = router.GetRoutes("type1");
var type2Routes = router.GetRoutes("type2");
var missingRoutes = router.GetRoutes("missing");
// assert
Assert.That(type1Routes, Is.Not.Null);
Assert.That(type2Routes, Is.Not.Null);
Assert.That(missingRoutes, Is.Null);
Assert.That(type1Routes, Is.TypeOf<SubscriptionRouteCollection>());
Assert.That(type2Routes, Is.TypeOf<SubscriptionRouteCollection>());
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
[Test]
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
{
// arrange
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.Ok()));
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
[Test]
public void Handle_Should_InvokeAllMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return CallResult.Ok();
}));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_NotInvokeTopicRoutes_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.Empty);
}
}
}
@@ -0,0 +1,130 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.UnitTests.Implementations;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class SubscriptionTests
{
[Test]
public void Handle_Should_OnlyCompleteSubscriptionQuery_ForMatchingTopic()
{
// arrange
var topicASubscription = new TopicSubscription("topic-a");
var topicBSubscription = new TopicSubscription("topic-b");
var topicAQuery = topicASubscription.CreateSubscriptionQuery(null!)!;
var topicBQuery = topicBSubscription.CreateSubscriptionQuery(null!)!;
// act
var topicAHandled = topicASubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
var topicBHandled = topicBSubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(topicAHandled, Is.True);
Assert.That(topicBHandled, Is.False);
Assert.That(topicAQuery.Completed, Is.True);
Assert.That(topicAQuery.Success, Is.True);
Assert.That(topicBQuery.Completed, Is.False);
}
[Test]
public void Handle_Should_CompleteSubscriptionQuery_BeforeInvokingMatchingHandler()
{
// arrange
Query? query = null;
var queryCompletedWhenHandlerInvoked = false;
var subscription = new TopicSubscription("topic", () => queryCompletedWhenHandlerInvoked = query!.Completed);
query = subscription.CreateSubscriptionQuery(null!)!;
// act
subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(queryCompletedWhenHandlerInvoked, Is.True);
}
[TestCase(null)]
[TestCase("topic")]
public void Handle_Should_CompleteSubscriptionQuery_ForUnfilteredRoute(string? topicFilter)
{
// arrange
var subscription = new TopicSubscription(topic: null);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", topicFilter, null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
[Test]
public void Handle_Should_TreatEmptyTopicFilterAsUnfilteredRoute()
{
// arrange
var subscription = new TopicSubscription(string.Empty);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
[Test]
public void Handle_Should_CompleteSubscriptionQuery_ForAnyMatchingTopic()
{
// arrange
var subscription = new TopicSubscription(["topic-a", "topic-b"]);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", "topic-b", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
private sealed class TopicSubscription : Subscription
{
public TopicSubscription(string? topic, Action? handler = null)
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.CreateForEvent<string>("type", topic, (_, _, _, _) =>
{
handler?.Invoke();
return CallResult.Ok();
});
}
public TopicSubscription(string[] topics)
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.CreateForEvent<string>("type", topics, (_, _, _, _) => CallResult.Ok());
}
protected override Query? GetSubQuery(SocketConnection connection)
{
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false)
{
TimeoutBehavior = TimeoutBehavior.Succeed
};
}
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
}
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.UnitTests
} }
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct) protected override Task<CallResult> DoResyncAsync(CancellationToken ct)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -1,440 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json;
using NUnit.Framework;
using System;
using System.Text.Json.Serialization;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SystemTextJsonConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
[TestCase("210512")]
[TestCase("1620777600.000")]
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600.000)]
[TestCase(1620777600000d)]
public void TestDateTimeConverterDouble(double input)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
[TestCase(1620777600000)]
[TestCase(1620777600000000)]
[TestCase(1620777600000000000)]
[TestCase(0, true)]
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600)]
[TestCase(1620777600.000)]
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
Assert.That(output.Time == null);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
[TestCase(null, null)]
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
Assert.That(result == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", null)]
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", false)]
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("nan", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterNumber(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[Test()]
public void TestArrayConverter()
{
var data = new Test()
{
Prop1 = 2,
Prop2 = null,
Prop3 = "123",
Prop3Again = "123",
Prop4 = null,
Prop5 = new Test2
{
Prop21 = 3,
Prop22 = "456"
},
Prop6 = new Test3
{
Prop31 = 4,
Prop32 = "789"
},
Prop7 = TestEnum.Two,
TestInternal = new Test
{
Prop1 = 10
},
Prop8 = new Test3
{
Prop31 = 5,
Prop32 = "101"
},
};
var options = new JsonSerializerOptions()
{
TypeInfoResolver = new SerializationContext()
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
Assert.That(deserialized.Prop1, Is.EqualTo(2));
Assert.That(deserialized.Prop2, Is.Null);
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
Assert.That(deserialized.Prop4, Is.Null);
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
public class STJDecimalObject
{
[JsonConverter(typeof(DecimalConverter))]
[JsonPropertyName("test")]
public decimal? Test { get; set; }
}
public class STJTimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
}
public class STJEnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
public TestEnum Value { get; set; }
}
public class STJBoolObject
{
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
public bool Value { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test>))]
record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string Prop3 { get; set; }
[ArrayProperty(2)]
public string Prop3Again { get; set; }
[ArrayProperty(3)]
public string Prop4 { get; set; }
[ArrayProperty(4)]
public Test2 Prop5 { get; set; }
[ArrayProperty(5)]
public Test3 Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test TestInternal { get; set; }
[ArrayProperty(8), JsonConversion]
public Test3 Prop8 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test2>))]
record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string Prop22 { get; set; }
}
record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string Prop32 { get; set; }
}
[JsonConverter(typeof(EnumConverter<TestEnum>))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonSerializable(typeof(Test))]
[JsonSerializable(typeof(Test2))]
[JsonSerializable(typeof(Test3))]
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
[JsonSerializable(typeof(STJBoolObject))]
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
[JsonSerializable(typeof(STJEnumObject))]
[JsonSerializable(typeof(STJDecimalObject))]
[JsonSerializable(typeof(STJTimeObject))]
internal partial class SerializationContext : JsonSerializerContext
{
}
}
@@ -1,99 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.UnitTests
{
public class TestBaseClient: BaseClient
{
public TestSubClient SubClient { get; }
public TestBaseClient(): base(null, "Test")
{
var options = new TestClientOptions();
_logger = NullLogger.Instance;
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
{
_logger = NullLogger.Instance;
Initialize(exchangeOptions);
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
}
public void Log(LogLevel verbosity, string data)
{
_logger.Log(verbosity, data);
}
}
public class TestSubClient : RestApiClient
{
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{
}
public CallResult<T> Deserialize<T>(string data)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>();
return deserializeResult;
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
}
public class TestAuthProvider : AuthenticationProvider
{
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{
}
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
}
public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret;
}
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
}
@@ -1,49 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestHelpers
{
[ExcludeFromCodeCoverage]
public static bool AreEqual<T>(T self, T to, params string[] ignore) where T : class
{
if (self != null && to != null)
{
var type = self.GetType();
var ignoreList = new List<string>(ignore);
foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (ignoreList.Contains(pi.Name))
{
continue;
}
var selfValue = type.GetProperty(pi.Name).GetValue(self, null);
var toValue = type.GetProperty(pi.Name).GetValue(to, null);
if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll"))
{
// Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
if (AreEqual(selfValue, toValue, ignore))
{
continue;
}
return false;
}
if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
{
return false;
}
}
return true;
}
return self == to;
}
}
}
@@ -1,234 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Moq;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Options;
using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization;
using System.Net.Http.Headers;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestRestClient: BaseRestClient
{
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new HttpRequestMessage().Headers;
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
})
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
})
.Returns(request.Object);
requestObj = request.Object;
}
public void SetErrorWithoutResponse(HttpStatusCode code, string message)
{
var we = new HttpRequestException();
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
}
public void SetErrorWithResponse(string responseData, HttpStatusCode code)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new List<KeyValuePair<string, string[]>>();
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
}
}
public class TestRestApi1Client : RestApiClient
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
}
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
{
ParameterPositions[method] = position;
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestRestApi2Client : RestApiClient
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestError
{
[JsonPropertyName("errorCode")]
public int ErrorCode { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}
public class ParseErrorTestRestClient: TestRestClient
{
public ParseErrorTestRestClient() { }
}
}
@@ -1,29 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
}
}
}
@@ -1,17 +0,0 @@
using CryptoExchange.Net.UnitTests.TestImplementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests
{
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(TestObject))]
internal partial class TestSerializerContext : JsonSerializerContext
{
}
}
@@ -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<NoApiCredentialsError>());
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<string>(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<TokenScope, System.Threading.CancellationToken, Task<CallResult<string>>> startToken,
TimeSpan? refreshInterval = null,
TimeSpan? timeValid = null,
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? keepAliveToken = null,
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? 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<TokenLease> result)
{
Assert.That(result.Success, Is.True, result.Error?.ToString());
Assert.That(result.Data, Is.Not.Null);
}
private static async Task WaitUntilAsync(Func<bool> 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;
}
}
}
@@ -0,0 +1,105 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests
{
internal class UriSerializationTests
{
[Test]
public void CreateParamString_SerializesBasicValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
}
[Test]
public void CreateParamString_SerializesArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a[]=1&a[]=2"));
}
[Test]
public void CreateParamStringEncoded_SerializesArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a[]=1%2B2&a[]=2%2B3"));
}
[Test]
public void CreateParamString_SerializesJsonArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.JsonArray);
Assert.That(parameterString, Is.EqualTo("a=[1,2]"));
}
[Test]
public void CreateParamStringEncoded_SerializesJsonArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.JsonArray);
Assert.That(parameterString, Is.EqualTo("a=[1%2B2,2%2B3]"));
}
[Test]
public void CreateParamString_SerializesMultipleValuesArrayCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.MultipleValues);
Assert.That(parameterString, Is.EqualTo("a=1&a=2"));
}
[Test]
public void CreateParamStringEncoded_SerializesMultipleValuesArrayCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.MultipleValues);
Assert.That(parameterString, Is.EqualTo("a=1%2B2&a=2%2B3"));
}
}
}
-6
View File
@@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -43,10 +41,6 @@ Global
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU {988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -1,7 +1,7 @@
#if NETSTANDARD2_0 namespace System.Diagnostics.CodeAnalysis
namespace System.Diagnostics.CodeAnalysis
{ {
using System; using System;
#if NETSTANDARD2_0
/// <summary> /// <summary>
/// Specifies that <see langword="null"/> is allowed as an input even if the /// Specifies that <see langword="null"/> is allowed as an input even if the
@@ -206,5 +206,26 @@ namespace System.Diagnostics.CodeAnalysis
ReturnValue = returnValue; ReturnValue = returnValue;
} }
} }
}
#endif #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
}
@@ -1,101 +1,19 @@
using System; namespace CryptoExchange.Net.Authentication
using System.IO;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Authentication
{ {
/// <summary> /// <summary>
/// Api credentials, used to sign requests accessing private endpoints /// Api credentials, used to sign requests accessing private endpoints
/// </summary> /// </summary>
public class ApiCredentials public abstract class ApiCredentials
{ {
/// <summary> /// <summary>
/// The api key / label to authenticate requests /// Validate the API credentials
/// </summary> /// </summary>
public string Key { get; set; } public abstract void Validate();
/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; set; }
/// <summary>
/// The api passphrase. Not needed on all exchanges
/// </summary>
public string? Pass { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="pass">The api pass for the key. Not always needed</param>
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialType;
Key = key;
Secret = secret;
Pass = pass;
}
/// <summary>
/// Create API credentials using an API key and secret generated by the server
/// </summary>
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
{
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in PEM format
/// </summary>
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in XML format
/// </summary>
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
}
/// <summary>
/// Create API credentials using an API key and an Ed25519 private key
/// </summary>
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
}
/// <summary>
/// Load a key from a file
/// </summary>
public static string ReadFromFile(string path)
{
using var fileStream = File.OpenRead(path);
using var streamReader = new StreamReader(fileStream);
return streamReader.ReadToEnd();
}
/// <summary> /// <summary>
/// Copy the credentials /// Copy the credentials
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public virtual ApiCredentials Copy() public abstract ApiCredentials Copy();
{
return new ApiCredentials(Key, Secret, Pass, CredentialType);
}
} }
} }
@@ -1,25 +0,0 @@
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Credentials type
/// </summary>
public enum ApiCredentialsType
{
/// <summary>
/// Hmac keys credentials
/// </summary>
Hmac,
/// <summary>
/// Rsa keys credentials in xml format
/// </summary>
RsaXml,
/// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary>
RsaPem,
/// <summary>
/// Ed25519 keys credentials
/// </summary>
Ed25519
}
}
@@ -11,6 +11,9 @@ using System.Linq;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using System.Net;
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication
{ {
@@ -22,66 +25,25 @@ namespace CryptoExchange.Net.Authentication
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
/// <summary> /// <summary>
/// The supported credential types /// The public identifier for the provided credentials
/// </summary> /// </summary>
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; } public abstract string Key { get; }
/// <summary> /// <summary>
/// Provided credentials /// Authenticate a REST request
/// </summary> /// </summary>
protected internal readonly ApiCredentials _credentials; /// <param name="apiClient">The API client sending the request</param>
/// <summary>
/// Byte representation of the secret
/// </summary>
protected byte[] _sBytes;
#if NET8_0_OR_GREATER
/// <summary>
/// The Ed25519 private key
/// </summary>
protected Key? Ed25519Key;
#endif
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key!;
/// <summary>
/// Get the Passphrase of the current credentials
/// </summary>
public string? Pass => _credentials.Pass;
/// <summary>
/// ctor
/// </summary>
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
{
#if !NET8_0_OR_GREATER
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
#endif
}
_credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
}
/// <summary>
/// Authenticate a request
/// </summary>
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="requestConfig">The request configuration</param> /// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig); public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// Get an authentication query for a websocket
/// </summary>
/// <param name="apiClient">The API client sending the request</param>
/// <param name="connection">The connection to authenticate</param>
/// <param name="context">Optional context required for creating the authentication query</param>
public virtual Query? GetAuthenticationQuery(SocketApiClient apiClient, SocketConnection connection, Dictionary<string, object?>? context = null) => null;
/// <summary> /// <summary>
/// SHA256 sign the data and return the bytes /// SHA256 sign the data and return the bytes
/// </summary> /// </summary>
@@ -263,6 +225,278 @@ namespace CryptoExchange.Net.Authentication
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
} }
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
protected string SignHMACSHA256(HMACCredential credential, string data, SignOutputType? outputType = null)
=> SignHMACSHA256(credential,Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
protected string SignHMACSHA256(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA256(credential.GetSBytes());
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
protected string SignHMACSHA384(HMACCredential credential, string data, SignOutputType? outputType = null)
=> SignHMACSHA384(credential, Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
protected string SignHMACSHA384(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA384(credential.GetSBytes());
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
protected string SignHMACSHA512(HMACCredential credential, string data, SignOutputType? outputType = null)
=> SignHMACSHA512(credential, Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
protected string SignHMACSHA512(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA512(credential.GetSBytes());
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA256 sign the data
/// </summary>
protected string SignRSASHA256(RSACredential credential, byte[] data, SignOutputType? outputType = null)
{
var rsa = credential.GetSigner();
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA384 sign the data
/// </summary>
protected string SignRSASHA384(RSACredential credential, byte[] data, SignOutputType? outputType = null)
{
var rsa = credential.GetSigner();
using var sha384 = SHA384.Create();
var hash = sha384.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA512 sign the data
/// </summary>
protected string SignRSASHA512(RSACredential credential, byte[] data, SignOutputType? outputType = null)
{
var rsa = credential.GetSigner();
using var sha512 = SHA512.Create();
var hash = sha512.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
#if NET8_0_OR_GREATER
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(Ed25519Credential credential, string data, SignOutputType? outputType = null)
=> SignEd25519(credential, Encoding.ASCII.GetBytes(data), outputType);
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(Ed25519Credential credential, byte[] data, SignOutputType? outputType = null)
{
var signKey = credential.GetSigningKey();
var resultBytes = SignatureAlgorithm.Ed25519.Sign(signKey, data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
#endif
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(byte[] buff)
=> BytesToHexString(new ArraySegment<byte>(buff));
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(ArraySegment<byte> buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2");
return result;
#endif
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
protected static byte[] HexToBytesString(string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
/// <summary>
/// Convert byte array to base64 string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToBase64String(byte[] buff)
{
return Convert.ToBase64String(buff);
}
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
{
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetRestOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
result = result.AddSeconds(-1);
return result;
}
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
{
var timestamp = TimeProvider.GetTime();
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
timestamp = timestamp.AddSeconds(-1);
return timestamp;
}
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
protected string GetMillisecondTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
protected long GetMillisecondTimestampLong(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary>
/// Return the serialized request body
/// </summary>
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
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?.BodyValue != null)
return stringSerializer.Serialize(parameters.BodyValue);
else
return stringSerializer.Serialize(parameters);
}
}
/// <inheritdoc />
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider
where TApiCredentials: ApiCredentials
{
/// <summary>
/// API credentials used for signing requests
/// </summary>
public TApiCredentials ApiCredentials { get; set; }
/// <summary>
/// ctor
/// </summary>
protected AuthenticationProvider(TApiCredentials credentials)
{
credentials.Validate();
ApiCredentials = credentials;
}
}
/// <inheritdoc />
public abstract class AuthenticationProvider<TApiCredentials, TCredentialType> : AuthenticationProvider<TApiCredentials>
where TApiCredentials : ApiCredentials
where TCredentialType : CredentialSet
{
/// <summary>
/// The specific credential type used for signing requests.
/// </summary>
public TCredentialType Credential { get; }
/// <inheritdoc />
public override string Key => Credential.Key;
/// <summary>
/// ctor
/// </summary>
protected AuthenticationProvider(
TApiCredentials credentials,
TCredentialType? credential) : base(credentials)
{
if (credential == null)
throw new ArgumentException($"Missing \"{typeof(TCredentialType).Name}\" credentials on \"{credentials.GetType().Name}\"");
Credential = credential;
}
/// <summary> /// <summary>
/// HMACSHA256 sign the data and return the hash /// HMACSHA256 sign the data and return the hash
/// </summary> /// </summary>
@@ -280,9 +514,10 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null) protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA256(_sBytes); if (Credential is not HMACCredential hmacCredential)
var resultBytes = encryptor.ComputeHash(data); throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
return SignHMACSHA256(hmacCredential, data, outputType);
} }
/// <summary> /// <summary>
@@ -302,9 +537,10 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null) protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA384(_sBytes); if (Credential is not HMACCredential hmacCredential)
var resultBytes = encryptor.ComputeHash(data); throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
return SignHMACSHA384(hmacCredential, data, outputType);
} }
/// <summary> /// <summary>
@@ -324,9 +560,10 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null) protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA512(_sBytes); if (Credential is not HMACCredential hmacCredential)
var resultBytes = encryptor.ComputeHash(data); throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
return SignHMACSHA512(hmacCredential, data, outputType);
} }
/// <summary> /// <summary>
@@ -337,11 +574,10 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null) protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
{ {
using var rsa = CreateRSA(); if (Credential is not RSACredential rsaCredential)
using var sha256 = SHA256.Create(); throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
var hash = sha256.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return SignRSASHA256(rsaCredential, data, outputType);
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
/// <summary> /// <summary>
@@ -352,11 +588,10 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null) protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
{ {
using var rsa = CreateRSA(); if (Credential is not RSACredential rsaCredential)
using var sha384 = SHA384.Create(); throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
var hash = sha384.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1); return SignRSASHA384(rsaCredential, data, outputType);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
/// <summary> /// <summary>
@@ -367,13 +602,13 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null) protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
{ {
using var rsa = CreateRSA(); if (Credential is not RSACredential rsaCredential)
using var sha512 = SHA512.Create(); throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
var hash = sha512.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); return SignRSASHA512(rsaCredential, data, outputType);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
#if NET8_0_OR_GREATER
/// <summary> /// <summary>
/// Ed25519 sign the data /// Ed25519 sign the data
/// </summary> /// </summary>
@@ -385,144 +620,11 @@ namespace CryptoExchange.Net.Authentication
/// </summary> /// </summary>
public string SignEd25519(byte[] data, SignOutputType? outputType = null) public string SignEd25519(byte[] data, SignOutputType? outputType = null)
{ {
#if NET8_0_OR_GREATER if (Credential is not Ed25519Credential ed25519Credential)
if (Ed25519Key == null) throw new InvalidOperationException($"Invalid Ed25519 signing without Ed25519 credentials provided");
{
var key = _credentials.Secret!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
}
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data); return SignEd25519(ed25519Credential, data, outputType);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); }
#else
throw new InvalidOperationException();
#endif #endif
} }
private RSA CreateRSA()
{
var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key
var key = _credentials.Secret!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
#else
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
#endif
}
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
{
// Read from xml private key format
rsa.FromXmlString(_credentials.Secret!);
}
else
{
throw new Exception("Invalid credentials type");
}
return rsa;
}
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(byte[] buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2");
return result;
#endif
}
/// <summary>
/// Convert byte array to base64 string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToBase64String(byte[] buff)
{
return Convert.ToBase64String(buff);
}
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected DateTime GetTimestamp(RestApiClient apiClient)
{
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
}
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected string GetMillisecondTimestamp(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
}
/// <summary>
/// Return the serialized request body
/// </summary>
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
{
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return stringSerializer.Serialize(value);
else
return stringSerializer.Serialize(parameters);
}
}
/// <inheritdoc />
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
{
/// <inheritdoc />
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
/// <summary>
/// ctor
/// </summary>
/// <param name="credentials"></param>
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
{
}
}
} }
@@ -0,0 +1,569 @@
using System;
using System.Security.Cryptography;
using System.Text;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Base class for a set of credentials
/// </summary>
public abstract class CredentialSet : ApiCredentials
{
/// <summary>
/// The (public) key/identifier for this credential pair
/// </summary>
public string Key { get; set; }
/// <summary>
/// ctor
/// </summary>
public CredentialSet() { }
/// <summary>
/// ctor
/// </summary>
public CredentialSet(string key)
{
Key = key;
}
/// <summary>
/// Validate the API credential
/// </summary>
public override void Validate()
{
if (string.IsNullOrEmpty(Key))
throw new ArgumentException($"Key not set on {GetType().Name}", nameof(Key));
}
}
/// <summary>
/// Api key credentials
/// </summary>
public class ApiKeyCredential : CredentialSet
{
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key</param>
public ApiKeyCredential(string key) : base(key)
{
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ApiKeyCredential(Key);
}
/// <summary>
/// HMAC credentials
/// </summary>
public class HMACCredential : CredentialSet
{
private byte[]? _sBytes;
/// <summary>
/// API secret
/// </summary>
public string Secret { get; set; }
/// <summary>
/// ctor
/// </summary>
public HMACCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="secret">Api secret</param>
public HMACCredential(string key, string secret) : base(key)
{
Secret = secret;
}
/// <summary>
/// Get the secret value bytes
/// </summary>
/// <returns></returns>
public byte[] GetSBytes()
{
return _sBytes ??= Encoding.UTF8.GetBytes(Secret);
}
/// <inheritdoc />
public override ApiCredentials Copy() => new HMACCredential(Key, Secret);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Secret))
throw new ArgumentException($"Secret not set on {GetType().Name}", nameof(Secret));
}
}
/// <summary>
/// HMAC credentials
/// </summary>
public class HMACPassCredential : HMACCredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public HMACPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="secret">Api secret</param>
/// <param name="pass">Passphrase</param>
public HMACPassCredential(string key, string secret, string pass) : base(key, secret)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new HMACPassCredential(Key, Secret, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
/// <summary>
/// RSA credentials
/// </summary>
public abstract class RSACredential : CredentialSet
{
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public RSACredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSACredential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <summary>
/// Get RSA signer
/// </summary>
public abstract RSA GetSigner();
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// RSA credentials
/// </summary>
public abstract class RSAPassCredential : RSACredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public RSAPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public RSAPassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
PrivateKey = privateKey;
Pass = pass;
}
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
#if NETSTANDARD2_1_OR_GREATER || NET7_0_OR_GREATER
/// <summary>
/// RSA credentials in PEM/base64 format
/// </summary>
public class RSAPemCredential : RSACredential
{
/// <summary>
/// ctor
/// </summary>
public RSAPemCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSAPemCredential(string key, string privateKey) : base(key, privateKey)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAPemCredential(Key, PrivateKey);
}
/// <summary>
/// RSA PEM/Base64 credentials
/// </summary>
public class RSAPemPassCredential : RSAPassCredential
{
/// <summary>
/// ctor
/// </summary>
public RSAPemPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Api secret</param>
/// <param name="pass">Passphrase</param>
public RSAPemPassCredential(string key, string privateKey, string pass) : base(key, privateKey, pass)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAPemPassCredential(Key, PrivateKey, Pass);
}
#endif
/// <summary>
/// RSA credentials in XML format
/// </summary>
public class RSAXmlCredential : RSACredential
{
/// <summary>
/// ctor
/// </summary>
public RSAXmlCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSAXmlCredential(string key, string privateKey) : base(key, privateKey)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
rsa.FromXmlString(PrivateKey);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAXmlCredential(Key, PrivateKey);
}
/// <summary>
/// RSA XML credentials
/// </summary>
public class RSAXmlPassCredential : RSAPassCredential
{
/// <summary>
/// ctor
/// </summary>
public RSAXmlPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Api secret</param>
/// <param name="pass">Passphrase</param>
public RSAXmlPassCredential(string key, string privateKey, string pass) : base(key, privateKey, pass)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
rsa.FromXmlString(PrivateKey);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAXmlPassCredential(Key, PrivateKey, Pass);
}
#if NET8_0_OR_GREATER
/// <summary>
/// Credentials in Ed25519 format
/// </summary>
public class Ed25519Credential : CredentialSet
{
private NSec.Cryptography.Key? _signKey;
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public Ed25519Credential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public Ed25519Credential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <summary>
/// Get signing key
/// </summary>
public NSec.Cryptography.Key GetSigningKey()
{
if (_signKey != null)
return _signKey;
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
_signKey = NSec.Cryptography.Key.Import(NSec.Cryptography.SignatureAlgorithm.Ed25519, keyBytes, NSec.Cryptography.KeyBlobFormat.PkixPrivateKey);
return _signKey;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new Ed25519Credential(Key, PrivateKey);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// Ed25519 credentials
/// </summary>
public class Ed25519PassCredential : Ed25519Credential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public Ed25519PassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public Ed25519PassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new Ed25519PassCredential(Key, PrivateKey, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
#endif
/// <summary>
/// Credentials in ECDsa format
/// </summary>
public class ECDsaCredential : CredentialSet
{
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public ECDsaCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public ECDsaCredential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ECDsaCredential(Key, PrivateKey);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// ECDsa credentials
/// </summary>
public class ECDsaPassCredential : ECDsaCredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public ECDsaPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public ECDsaPassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ECDsaPassCredential(Key, PrivateKey, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
}
@@ -0,0 +1,147 @@
using System;
using System.Linq;
using System.Numerics;
using System.Text;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// ABI encoding
/// </summary>
public static class CeAbiEncoder
{
/// <summary>
/// ABI encode string as Sha3Keccack hashed byte value
/// </summary>
public static byte[] AbiValueEncodeString(string value)
{
var abiValueEncoded = CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(value));
return abiValueEncoded;
}
/// <summary>
/// ABI encode bool value as uint256 with 1 for true and 0 for false, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeBool(bool value)
=> AbiValueEncodeInt((byte)(value ? 1 : 0));
/// <summary>
/// ABI encode byte value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(byte value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode short value as int256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(short value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode int value as int256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(int value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode long value as int256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(long value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode ushort value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(ushort value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode uint value as uint256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(uint value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode ulong value as uint256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(ulong value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode big integer value as int256 or uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeBigInteger(bool signed, BigInteger value)
{
var result = new byte[32];
if (signed && value < 0)
{
// Pad with FF
for (int i = 0; i < result.Length; i++)
{
result[i] = 0xFF;
}
}
var t = value.ToByteArray();
if (t.Length == 33)
{
// Strip last byte
var strip1 = new byte[32];
Array.Copy(t, 0, strip1, 0, 32);
t = strip1;
}
if (BitConverter.IsLittleEndian)
t = t.AsEnumerable().Reverse().ToArray();
t.CopyTo(result, result.Length - t.Length);
return result;
}
/// <summary>
/// ABI encode address value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeAddress(string value)
{
var result = new byte[32];
var h = value.HexStringToBytes();
h.CopyTo(result, result.Length - h.Length);
return result;
}
/// <summary>
/// ABI encode hex string value as bytes32, as per ABI specification. The hex string is expected to be a 0x prefixed string, and the resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the hex string is shorter than 32 bytes. If the hex string is longer than 32 bytes, an exception will be thrown.
/// </summary>
/// <param name="length"></param>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeHexBytes(int length, string value)
=> AbiValueEncodeBytes(value.Length, value.HexStringToBytes());
/// <summary>
/// ABI encode byte array value as bytes32, as per ABI specification. The resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the byte array is shorter than 32 bytes. If the byte array is longer than 32 bytes, an exception will be thrown.
/// </summary>
/// <param name="length"></param>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static byte[] AbiValueEncodeBytes(int length, byte[] value)
{
if (length != 32)
throw new Exception("Only 32 bytes size supported");
if (value.Length == 32)
return value;
var result = new byte[32];
value.CopyTo(result, result.Length - value.Length);
return result;
}
}
}
@@ -0,0 +1,304 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// EIP712 Typed Data Encoder
/// </summary>
public static class CeEip712TypedDataEncoder
{
/// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary>
public static byte[] EncodeEip721(
string primaryType,
IEnumerable<(string Name, string Type, object Value)> domainFields,
IEnumerable<(string Name, string Type, object Value)> messageFields)
{
var data = new CeTypedDataRaw()
{
PrimaryType = primaryType,
DomainRawValues = domainFields.Select(x => new CeMemberValue
{
TypeName = x.Type,
Value = x.Value,
}).ToArray(),
Message = messageFields.Select(x => new CeMemberValue
{
TypeName = x.Type,
Value = x.Value,
}).ToArray(),
Types = new Dictionary<string, CeMemberDescription[]>
{
{
"EIP712Domain",
domainFields.Select(x => new CeMemberDescription
{
Name = x.Name,
Type = x.Type
}).ToArray()
},
{
primaryType,
messageFields.Select(x => new CeMemberDescription
{
Name = x.Name,
Type = x.Type
}).ToArray()
}
}
};
return EncodeTypedDataRaw(data);
}
/// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary>
public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData)
{
using var memoryStream = new MemoryStream();
using var writer = new BinaryWriter(memoryStream);
// Write 0x19 0x01 prefix
writer.Write((byte)0x19);
writer.Write((byte)0x01);
// Write domain
writer.Write(HashStruct(typedData.Types, "EIP712Domain", typedData.DomainRawValues));
// Write message
writer.Write(HashStruct(typedData.Types, typedData.PrimaryType, typedData.Message));
writer.Flush();
var result = memoryStream.ToArray();
return result;
}
private static byte[] HashStruct(IDictionary<string, CeMemberDescription[]> types, string primaryType, IEnumerable<CeMemberValue> message)
{
var memoryStream = new MemoryStream();
var writer = new BinaryWriter(memoryStream);
// Encode the type header
EncodeType(writer, types, primaryType);
// Encode the data
EncodeData(writer, types, message);
writer.Flush();
return CeSha3Keccack.CalculateHash(memoryStream.ToArray());
}
private static void EncodeData(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, IEnumerable<CeMemberValue> memberValues)
{
foreach (var memberValue in memberValues)
{
switch (memberValue.TypeName)
{
case var refType when IsReferenceType(refType):
writer.Write(HashStruct(types, memberValue.TypeName, (IEnumerable<CeMemberValue>)memberValue.Value));
break;
case "string":
writer.Write(CeAbiEncoder.AbiValueEncodeString((string)memberValue.Value));
break;
case "bool":
writer.Write(CeAbiEncoder.AbiValueEncodeBool((bool)memberValue.Value));
break;
case "address":
writer.Write(CeAbiEncoder.AbiValueEncodeAddress((string)memberValue.Value));
break;
default:
if (memberValue.TypeName.Contains("["))
{
var items = (IList)memberValue.Value;
var itemsMemberValues = new List<CeMemberValue>();
foreach (var item in items)
{
itemsMemberValues.Add(new CeMemberValue()
{
TypeName = memberValue.TypeName.Substring(0, memberValue.TypeName.LastIndexOf("[")),
Value = item
});
}
var memoryStream = new MemoryStream();
var writerItem = new BinaryWriter(memoryStream);
EncodeData(writerItem, types, itemsMemberValues);
writerItem.Flush();
writer.Write(CeSha3Keccack.CalculateHash(memoryStream.ToArray()));
}
else if (memberValue.TypeName.StartsWith("int") || memberValue.TypeName.StartsWith("uint"))
{
if (memberValue.Value is string v)
{
if (!BigInteger.TryParse(v, out BigInteger parsedOutput))
throw new Exception($"Failed to encode BigInteger string {v}");
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput));
}
else if (memberValue.Value is byte b)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(b));
}
else if (memberValue.Value is short s)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(s));
}
else if (memberValue.Value is int i)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(i));
}
else if (memberValue.Value is long l)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(l));
}
else if (memberValue.Value is ushort us)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(us));
}
else if (memberValue.Value is uint ui)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ui));
}
else if (memberValue.Value is ulong ul)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ul));
}
else
{
throw new Exception("Unknown number value");
}
}
else if (memberValue.TypeName.StartsWith("bytes"))
{
var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5));
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value));
}
break;
}
}
}
private static void EncodeType(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, string typeName)
{
var encodedTypes = EncodeTypes(types, typeName);
var encodedPrimaryType = encodedTypes.Single(x => x.Key == typeName);
var encodedReferenceTypes = encodedTypes.Where(x => x.Key != typeName).OrderBy(x => x.Key).Select(x => x.Value);
var fullyEncodedType = encodedPrimaryType.Value + string.Join(string.Empty, encodedReferenceTypes.ToArray());
writer.Write(CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(fullyEncodedType)));
}
/// <summary>
/// Create a list of type => type(parameters), for example:<br />
/// { IP712Domain, EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) }
/// </summary>
private static IList<KeyValuePair<string, string>> EncodeTypes(IDictionary<string, CeMemberDescription[]> types, string currentTypeName)
{
var currentTypeMembers = types[currentTypeName];
var currentTypeMembersEncoded = currentTypeMembers.Select(x => x.Type + " " + x.Name);
var result = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(currentTypeName, currentTypeName + "(" + string.Join(",", currentTypeMembersEncoded.ToArray()) + ")")
};
result.AddRange(currentTypeMembers.Select(x => x.Type.Contains("[") ? x.Type.Substring(0, x.Type.IndexOf("[")) : x.Type)
.Distinct()
.Where(IsReferenceType)
.SelectMany(x => EncodeTypes(types, x)));
return result;
}
internal static bool IsReferenceType(string typeName)
{
switch (typeName)
{
case var bytes when new Regex("bytes\\d+").IsMatch(bytes):
case var @uint when new Regex("uint\\d+").IsMatch(@uint):
case var @int when new Regex("int\\d+").IsMatch(@int):
case "bytes":
case "string":
case "bool":
case "address":
case var array when array.Contains("["):
return false;
default:
return true;
}
}
}
/// <summary>
/// Member description
/// </summary>
public class CeMemberDescription
{
/// <summary>
/// Name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Type
/// </summary>
public string Type { get; set; } = string.Empty;
}
/// <summary>
/// Member value
/// </summary>
public class CeMemberValue
{
/// <summary>
/// Type name
/// </summary>
public string TypeName { get; set; } = string.Empty;
/// <summary>
/// Value
/// </summary>
public object Value { get; set; } = string.Empty;
}
/// <summary>
/// Typed data raw, used for encoding EIP712 typed data with the provided primary type, domain fields and message fields.
/// </summary>
public class CeTypedDataRaw
{
/// <summary>
/// Type dictionary
/// </summary>
public IDictionary<string, CeMemberDescription[]> Types { get; set; } = new Dictionary<string, CeMemberDescription[]>();
/// <summary>
/// Primary type
/// </summary>
public string PrimaryType { get; set; } = string.Empty;
/// <summary>
/// Message values
/// </summary>
public CeMemberValue[] Message { get; set; } = [];
/// <summary>
/// Domain values
/// </summary>
public CeMemberValue[] DomainRawValues { get; set; } = [];
}
}
@@ -0,0 +1,385 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// Sha3 Keccack hashing, as per Ethereum specification, with 256 bit output
/// </summary>
public class CeSha3Keccack
{
/// <summary>
/// Calculate the Keccack256 hash of the provided data, as per Ethereum specification
/// </summary>
public static byte[] CalculateHash(byte[] data)
{
var digest = new CeKeccakDigest256();
var output = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, data.Length);
digest.DoFinal(output, 0);
return output;
}
}
internal class CeKeccakDigest256
{
private static readonly ulong[] _keccakRoundConstants = KeccakInitializeRoundConstants();
private static readonly int[] _keccakRhoOffsets = KeccakInitializeRhoOffsets();
private readonly int _rate;
private const int _stateLength = 1600 / 8;
private readonly ulong[] _state = new ulong[_stateLength / 8];
private readonly byte[] _dataQueue = new byte[1536 / 8];
private int _bitsInQueue;
private int _fixedOutputLength;
private bool _squeezing;
private int _bitsAvailableForSqueezing;
public CeKeccakDigest256()
{
_rate = 1600 - (256 << 1);
_bitsInQueue = 0;
_squeezing = false;
_bitsAvailableForSqueezing = 0;
_fixedOutputLength = 1600 - _rate >> 1;
}
internal void BlockUpdate(byte[] data, int length)
{
int bytesInQueue = _bitsInQueue >> 3;
int rateBytes = _rate >> 3;
int count = 0;
while (count < length)
{
if (bytesInQueue == 0 && count <= length - rateBytes)
{
do
{
KeccakAbsorb(data, count);
count += rateBytes;
} while (count <= length - rateBytes);
}
else
{
int partialBlock = Math.Min(rateBytes - bytesInQueue, length - count);
Array.Copy(data, count, _dataQueue, bytesInQueue, partialBlock);
bytesInQueue += partialBlock;
count += partialBlock;
if (bytesInQueue == rateBytes)
{
KeccakAbsorb(_dataQueue, 0);
bytesInQueue = 0;
}
}
}
_bitsInQueue = bytesInQueue << 3;
}
internal void DoFinal(byte[] output, int outOff)
{
Squeeze(output, outOff, _fixedOutputLength >> 3);
}
internal int GetDigestSize() => _fixedOutputLength >> 3;
protected void Squeeze(byte[] output, int off, int len)
{
if (!_squeezing)
PadAndSwitchToSqueezingPhase();
long outputLength = (long)len << 3;
long i = 0;
while (i < outputLength)
{
if (_bitsAvailableForSqueezing == 0)
{
KeccakPermutation();
KeccakExtract();
_bitsAvailableForSqueezing = _rate;
}
int partialBlock = (int)Math.Min(_bitsAvailableForSqueezing, outputLength - i);
Array.Copy(_dataQueue, _rate - _bitsAvailableForSqueezing >> 3, output, off + (int)(i >> 3),
partialBlock >> 3);
_bitsAvailableForSqueezing -= partialBlock;
i += partialBlock;
}
}
private static ulong[] KeccakInitializeRoundConstants()
{
ulong[] keccakRoundConstants = new ulong[24];
byte LFSRState = 0x01;
for (int i = 0; i < 24; i++)
{
keccakRoundConstants[i] = 0;
for (int j = 0; j < 7; j++)
{
int bitPosition = (1 << j) - 1;
// LFSR86540
bool loBit = (LFSRState & 0x01) != 0;
if (loBit)
keccakRoundConstants[i] ^= 1UL << bitPosition;
bool hiBit = (LFSRState & 0x80) != 0;
LFSRState <<= 1;
if (hiBit)
LFSRState ^= 0x71;
}
}
return keccakRoundConstants;
}
private static int[] KeccakInitializeRhoOffsets()
{
int[] keccakRhoOffsets = new int[25];
int x, y, t, newX, newY;
int rhoOffset = 0;
keccakRhoOffsets[0] = rhoOffset;
x = 1;
y = 0;
for (t = 1; t < 25; t++)
{
rhoOffset = rhoOffset + t & 63;
keccakRhoOffsets[x % 5 + 5 * (y % 5)] = rhoOffset;
newX = (0 * x + 1 * y) % 5;
newY = (2 * x + 3 * y) % 5;
x = newX;
y = newY;
}
return keccakRhoOffsets;
}
private void KeccakAbsorb(byte[] data, int off)
{
int count = _rate >> 6;
for (int i = 0; i < count; ++i)
{
_state[i] ^= Pack.LeToUInt64(data, off);
off += 8;
}
KeccakPermutation();
}
private void KeccakPermutation()
{
for (int i = 0; i < 24; i++)
{
Theta(_state);
Rho(_state);
Pi(_state);
Chi(_state);
Iota(_state, i);
}
}
private static ulong LeftRotate(ulong v, int r)
{
return v << r | v >> -r;
}
private static void Theta(ulong[] A)
{
ulong C0 = A[0 + 0] ^ A[0 + 5] ^ A[0 + 10] ^ A[0 + 15] ^ A[0 + 20];
ulong C1 = A[1 + 0] ^ A[1 + 5] ^ A[1 + 10] ^ A[1 + 15] ^ A[1 + 20];
ulong C2 = A[2 + 0] ^ A[2 + 5] ^ A[2 + 10] ^ A[2 + 15] ^ A[2 + 20];
ulong C3 = A[3 + 0] ^ A[3 + 5] ^ A[3 + 10] ^ A[3 + 15] ^ A[3 + 20];
ulong C4 = A[4 + 0] ^ A[4 + 5] ^ A[4 + 10] ^ A[4 + 15] ^ A[4 + 20];
ulong dX = LeftRotate(C1, 1) ^ C4;
A[0] ^= dX;
A[5] ^= dX;
A[10] ^= dX;
A[15] ^= dX;
A[20] ^= dX;
dX = LeftRotate(C2, 1) ^ C0;
A[1] ^= dX;
A[6] ^= dX;
A[11] ^= dX;
A[16] ^= dX;
A[21] ^= dX;
dX = LeftRotate(C3, 1) ^ C1;
A[2] ^= dX;
A[7] ^= dX;
A[12] ^= dX;
A[17] ^= dX;
A[22] ^= dX;
dX = LeftRotate(C4, 1) ^ C2;
A[3] ^= dX;
A[8] ^= dX;
A[13] ^= dX;
A[18] ^= dX;
A[23] ^= dX;
dX = LeftRotate(C0, 1) ^ C3;
A[4] ^= dX;
A[9] ^= dX;
A[14] ^= dX;
A[19] ^= dX;
A[24] ^= dX;
}
private static void Rho(ulong[] A)
{
// KeccakRhoOffsets[0] == 0
for (int x = 1; x < 25; x++)
{
A[x] = LeftRotate(A[x], _keccakRhoOffsets[x]);
}
}
private static void Pi(ulong[] A)
{
ulong a1 = A[1];
A[1] = A[6];
A[6] = A[9];
A[9] = A[22];
A[22] = A[14];
A[14] = A[20];
A[20] = A[2];
A[2] = A[12];
A[12] = A[13];
A[13] = A[19];
A[19] = A[23];
A[23] = A[15];
A[15] = A[4];
A[4] = A[24];
A[24] = A[21];
A[21] = A[8];
A[8] = A[16];
A[16] = A[5];
A[5] = A[3];
A[3] = A[18];
A[18] = A[17];
A[17] = A[11];
A[11] = A[7];
A[7] = A[10];
A[10] = a1;
}
private static void Chi(ulong[] A)
{
ulong chiC0, chiC1, chiC2, chiC3, chiC4;
for (int yBy5 = 0; yBy5 < 25; yBy5 += 5)
{
chiC0 = A[0 + yBy5] ^ ~A[(0 + 1) % 5 + yBy5] & A[(0 + 2) % 5 + yBy5];
chiC1 = A[1 + yBy5] ^ ~A[(1 + 1) % 5 + yBy5] & A[(1 + 2) % 5 + yBy5];
chiC2 = A[2 + yBy5] ^ ~A[(2 + 1) % 5 + yBy5] & A[(2 + 2) % 5 + yBy5];
chiC3 = A[3 + yBy5] ^ ~A[(3 + 1) % 5 + yBy5] & A[(3 + 2) % 5 + yBy5];
chiC4 = A[4 + yBy5] ^ ~A[(4 + 1) % 5 + yBy5] & A[(4 + 2) % 5 + yBy5];
A[0 + yBy5] = chiC0;
A[1 + yBy5] = chiC1;
A[2 + yBy5] = chiC2;
A[3 + yBy5] = chiC3;
A[4 + yBy5] = chiC4;
}
}
private static void Iota(ulong[] A, int indexRound)
{
A[0] ^= _keccakRoundConstants[indexRound];
}
private void PadAndSwitchToSqueezingPhase()
{
Debug.Assert(_bitsInQueue < _rate);
_dataQueue[_bitsInQueue >> 3] |= (byte)(1U << (_bitsInQueue & 7));
if (++_bitsInQueue == _rate)
{
KeccakAbsorb(_dataQueue, 0);
_bitsInQueue = 0;
}
{
int full = _bitsInQueue >> 6, partial = _bitsInQueue & 63;
int off = 0;
for (int i = 0; i < full; ++i)
{
_state[i] ^= Pack.LeToUInt64(_dataQueue, off);
off += 8;
}
if (partial > 0)
{
ulong mask = (1UL << partial) - 1UL;
_state[full] ^= Pack.LeToUInt64(_dataQueue, off) & mask;
}
_state[_rate - 1 >> 6] ^= 1UL << 63;
}
KeccakPermutation();
KeccakExtract();
_bitsAvailableForSqueezing = _rate;
_bitsInQueue = 0;
_squeezing = true;
}
private void KeccakExtract()
{
Pack.UInt64ToLe(_state, 0, _rate >> 6, _dataQueue, 0);
}
static class Pack
{
internal static ulong LeToUInt64(byte[] bs, int off)
{
uint lo = LeToUInt32(bs, off);
uint hi = LeToUInt32(bs, off + 4);
return (ulong)hi << 32 | lo;
}
internal static uint LeToUInt32(byte[] bs, int off)
{
return bs[off]
| (uint)bs[off + 1] << 8
| (uint)bs[off + 2] << 16
| (uint)bs[off + 3] << 24;
}
internal static void UInt64ToLe(ulong[] ns, int nsOff, int nsLen, byte[] bs, int bsOff)
{
for (int i = 0; i < nsLen; ++i)
{
UInt64ToLe(ns[nsOff + i], bs, bsOff);
bsOff += 8;
}
}
internal static void UInt64ToLe(ulong n, byte[] bs, int off)
{
UInt32ToLe((uint)n, bs, off);
UInt32ToLe((uint)(n >> 32), bs, off + 4);
}
internal static void UInt32ToLe(uint n, byte[] bs, int off)
{
bs[off] = (byte)n;
bs[off + 1] = (byte)(n >> 8);
bs[off + 2] = (byte)(n >> 16);
bs[off + 3] = (byte)(n >> 24);
}
}
}
}
+81 -40
View File
@@ -1,10 +1,13 @@
using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients
{ {
@@ -13,6 +16,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient public abstract class BaseApiClient : IDisposable, IBaseApiClient
{ {
/// <summary>
/// Client name
/// </summary>
protected string? _clientName;
/// <summary> /// <summary>
/// Logger /// Logger
/// </summary> /// </summary>
@@ -21,12 +29,32 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// If we are disposing /// If we are disposing
/// </summary> /// </summary>
protected bool _disposing; protected bool _disposed;
/// <summary> /// <summary>
/// The authentication provider for this API client. (null if no credentials are set) /// Whether a proxy is configured
/// </summary> /// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; } protected bool _proxyConfigured;
/// <summary>
/// Name of the client
/// </summary>
protected internal string ClientName
{
get
{
if (_clientName != null)
return _clientName;
_clientName = GetType().Name;
return _clientName;
}
}
/// <summary>
/// The name of the exchange this client is for
/// </summary>
public string Exchange { get; }
/// <summary> /// <summary>
/// The environment this client communicates to /// The environment this client communicates to
@@ -38,12 +66,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public bool OutputOriginalData { get; } public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary> /// <summary>
/// Api options /// Api options
/// </summary> /// </summary>
@@ -59,36 +81,42 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]); protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// Current rate limit admission override
/// </summary>
protected readonly AsyncLocal<RateLimitAdmission?> AdmissionOverride = new();
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger">Logger</param> /// <param name="loggerFactory">Logger factory</param>
/// <param name="exchange">The exchange name</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param> /// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param> /// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param> /// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param> /// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions) protected BaseApiClient(
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; ClientOptions = clientOptions;
ApiOptions = apiOptions; ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData; OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress; BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
if (ApiCredentials != null) _proxyConfigured = ClientOptions.Proxy != null;
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
} }
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc /> /// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
@@ -103,30 +131,43 @@ namespace CryptoExchange.Net.Clients
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message); public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc /> /// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials public async Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation)
{ {
ApiCredentials = credentials?.Copy(); if (admission == null)
if (ApiCredentials != null) throw new ArgumentNullException(nameof(admission));
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); if (operation == null)
throw new ArgumentNullException(nameof(operation));
var previous = AdmissionOverride.Value;
AdmissionOverride.Value = admission;
try
{
return await operation().ConfigureAwait(false);
} }
finally
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{ {
ClientOptions.Proxy = options.Proxy; AdmissionOverride.Value = previous;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout; }
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
} }
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public void Dispose()
{ {
_disposing = true; Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
protected virtual void Dispose(bool disposing)
{
_disposed = true;
} }
} }
} }
+14 -11
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@@ -39,6 +40,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public string Exchange { get; } public string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
public bool Disposed { get; private set; }
/// <summary> /// <summary>
/// Api clients in this client /// Api clients in this client
/// </summary> /// </summary>
@@ -84,17 +90,6 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
ClientOptions = options; ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
} }
/// <summary> /// <summary>
@@ -120,11 +115,19 @@ namespace CryptoExchange.Net.Clients
return opts; return opts;
} }
/// <inheritdoc />
public override string ToString()
{
return $"{GetType().Name}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}, configuration: {ClientOptions}";
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public virtual void Dispose()
{ {
Disposed = true;
foreach (var client in ApiClients) foreach (var client in ApiClients)
client.Dispose(); client.Dispose();
} }
+64 -1
View File
@@ -1,7 +1,11 @@
using System.Linq; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients
{ {
@@ -10,6 +14,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient public abstract class BaseRestClient : BaseClient, IRestClient
{ {
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
/// <inheritdoc /> /// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade); public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
@@ -24,5 +33,59 @@ namespace CryptoExchange.Net.Clients
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange"); LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
} }
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class BaseRestClient<TEnvironment, TApiCredentials> : BaseRestClient, IRestClient<TApiCredentials>
where TEnvironment : TradeEnvironment
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
/// <summary>
/// Provided client options
/// </summary>
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public virtual void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
} }
} }
+61 -1
View File
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -19,6 +21,11 @@ namespace CryptoExchange.Net.Clients
{ {
#region fields #region fields
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
/// <summary> /// <summary>
/// If client is disposing /// If client is disposing
/// </summary> /// </summary>
@@ -133,5 +140,58 @@ namespace CryptoExchange.Net.Clients
return result; return result;
} }
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class BaseSocketClient<TEnvironment, TApiCredentials> : BaseSocketClient, ISocketClient<TApiCredentials>
where TEnvironment : TradeEnvironment
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
/// <summary>
/// Provided client options
/// </summary>
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public virtual void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
} }
} }
@@ -1,67 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base crypto client
/// </summary>
public class CryptoBaseClient : IDisposable
{
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary>
/// Service provider
/// </summary>
protected readonly IServiceProvider? _serviceProvider;
/// <summary>
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_serviceCache = new Dictionary<Type, object>();
}
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
if (_serviceProvider == null)
{
// Create with default options
var createResult = createFunc();
_serviceCache.Add(typeof(T), createResult!);
return createResult;
}
var result = _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
_serviceCache.Add(type, result!);
return result;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_serviceCache.Clear();
}
}
}
@@ -1,24 +0,0 @@
using CryptoExchange.Net.Interfaces.Clients;
using System;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoRestClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
}
@@ -1,24 +0,0 @@
using CryptoExchange.Net.Interfaces.Clients;
using System;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoSocketClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
}
File diff suppressed because it is too large Load Diff
+388 -206
View File
@@ -1,3 +1,4 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
@@ -14,11 +15,13 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf; using CryptoExchange.Net.Sockets.HighPerf;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces; using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces; using CryptoExchange.Net.Sockets.Interfaces;
using CryptoExchange.Net.TokenManagement;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -32,8 +35,10 @@ namespace CryptoExchange.Net.Clients
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{ {
#region Fields #region Fields
/// <inheritdoc/> /// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory(); public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <inheritdoc/> /// <inheritdoc/>
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; } public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
@@ -67,11 +72,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected List<SystemSubscription> systemSubscriptions = new(); protected List<SystemSubscription> systemSubscriptions = new();
/// <summary>
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
/// </summary>
protected internal bool UnhandledMessageExpected { get; set; }
/// <summary> /// <summary>
/// The rate limiters /// The rate limiters
/// </summary> /// </summary>
@@ -98,9 +98,9 @@ namespace CryptoExchange.Net.Clients
protected bool AllowTopicsOnTheSameConnection { get; set; } = true; protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary> /// <summary>
/// Whether to continue processing and forward unparsable messages to handlers /// Whether to allow usage of a current public WebSocket connection for an authenticated request
/// </summary> /// </summary>
protected internal bool ProcessUnparsableMessages { get; set; } = false; protected bool UsePublicConnectionForAuth { get; set; } = false;
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
@@ -140,31 +140,47 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public int? MaxIndividualSubscriptionsPerConnection { get; set; } public int? MaxIndividualSubscriptionsPerConnection { get; set; }
/// <summary>
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
/// </summary>
public bool EnforceSequenceNumbers { get; set; }
/// <summary>
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
/// </summary>
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
/// <summary>
/// Configured environment name
/// </summary>
public abstract string EnvironmentName { get; }
private int _isDisposed;
#endregion #endregion
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger">log</param> /// <param name="loggerFactory">Logger factory</param>
/// <param name="exchangeName">Exchange name</param>
/// <param name="options">Client options</param> /// <param name="options">Client options</param>
/// <param name="baseAddress">Base address for this API client</param> /// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiOptions">The Api client options</param> /// <param name="apiOptions">The Api client options</param>
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) public SocketApiClient(
: base(logger, ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions)
: base(loggerFactory,
exchangeName,
apiOptions.OutputOriginalData ?? options.OutputOriginalData, apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress, baseAddress,
options, options,
apiOptions) apiOptions)
{ {
} }
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
/// <summary> /// <summary>
/// Create a serializer instance /// Create a serializer instance
/// </summary> /// </summary>
@@ -181,6 +197,24 @@ namespace CryptoExchange.Net.Clients
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth }); DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
} }
/// <summary>
/// Update the timestamp offset between client and server based on the timestamp
/// </summary>
/// <param name="timestamp">Timestamp received from the server</param>
public virtual void UpdateTimeOffset(DateTime timestamp)
{
if (timestamp == default)
return;
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
}
/// <summary>
/// Get the time offset between client and server
/// </summary>
/// <returns></returns>
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
/// <summary> /// <summary>
/// Add a query to periodically send on each connection /// Add a query to periodically send on each connection
/// </summary> /// </summary>
@@ -188,7 +222,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, WebSocketResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -205,7 +239,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="subscription">The subscription</param> /// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param> /// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct) protected virtual Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
{ {
return SubscribeAsync(BaseAddress, subscription, ct); return SubscribeAsync(BaseAddress, subscription, ct);
} }
@@ -217,19 +251,22 @@ namespace CryptoExchange.Net.Clients
/// <param name="subscription">The subscription</param> /// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param> /// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct) protected virtual async Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
{ {
if (_disposing) bool successResult = false;
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe")); try
{
if (_disposed)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null) if (subscription.Authenticated && GetAuthenticationProvider() == null)
{ {
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set"); _logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError()); return WebSocketResult.Fail<UpdateSubscription>(Exchange, new NoApiCredentialsError());
} }
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection) if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}")); return WebSocketResult.Fail<UpdateSubscription>(Exchange, ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
@@ -241,7 +278,7 @@ namespace CryptoExchange.Net.Clients
} }
catch (OperationCanceledException tce) catch (OperationCanceledException tce)
{ {
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce)); return WebSocketResult.Fail<UpdateSubscription>(Exchange, new CancellationRequestedError(tce));
} }
try try
@@ -250,8 +287,8 @@ namespace CryptoExchange.Net.Clients
{ {
// Get a new or existing socket connection // Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult) if (!socketResult.Success)
return socketResult.As<UpdateSubscription>(null); return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
socketConnection = socketResult.Data; socketConnection = socketResult.Data;
@@ -273,8 +310,8 @@ namespace CryptoExchange.Net.Clients
var needsConnecting = !socketConnection.Connected; var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false); var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
if (!connectResult) if (!connectResult.Success)
return new CallResult<UpdateSubscription>(connectResult.Error!); return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
break; break;
} }
@@ -288,58 +325,28 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); return WebSocketResult.Fail<UpdateSubscription>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
void HandleSubscriptionComplete(bool success, object? response) var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
{ if (!subscribeResult.Success)
if (!success) return WebSocketResult.Fail<UpdateSubscription>(Exchange, subscribeResult.Error!);
return;
subscription.HandleSubQueryResponse(response);
subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
}
subscription.Status = SubscriptionStatus.Subscribing;
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null)
{
subQuery.OnComplete = () => HandleSubscriptionComplete(subQuery.Result?.Success ?? false, subQuery.Response);
// Send the request and wait for answer
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, ct).ConfigureAwait(false);
if (!subResult)
{
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
else
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a server process error we still might need to send an unsubscribe to prevent messages coming in later
subscription.Status = SubscriptionStatus.Pending;
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
}
else
{
HandleSubscriptionComplete(true, null);
}
successResult = true;
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription)); return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
subscribeResult.ResponseTime!.Value,
subscribeResult.RequestId!.Value,
subscribeResult.Url,
new UpdateSubscription(socketConnection, subscription));
}
finally
{
if (!successResult && subscription.TokenLease != null)
_ = subscription.TokenLease.ReleaseAsync();
}
} }
/// <summary> /// <summary>
@@ -350,14 +357,14 @@ namespace CryptoExchange.Net.Clients
/// <param name="connectionFactory">The factory for creating a socket connection</param> /// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token for closing this subscription</param> /// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>( protected virtual async Task<WebSocketResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
string url, string url,
HighPerfSubscription<TUpdateType> subscription, HighPerfSubscription<TUpdateType> subscription,
IHighPerfConnectionFactory connectionFactory, IHighPerfConnectionFactory connectionFactory,
CancellationToken ct) CancellationToken ct)
{ {
if (_disposing) if (_disposed)
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe")); return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
HighPerfSocketConnection<TUpdateType> socketConnection; HighPerfSocketConnection<TUpdateType> socketConnection;
var released = false; var released = false;
@@ -369,7 +376,7 @@ namespace CryptoExchange.Net.Clients
} }
catch (OperationCanceledException tce) catch (OperationCanceledException tce)
{ {
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce)); return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new CancellationRequestedError(tce));
} }
try try
@@ -378,8 +385,8 @@ namespace CryptoExchange.Net.Clients
{ {
// Get a new or existing socket connection // Get a new or existing socket connection
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false); var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
if (!socketResult) if (!socketResult.Success)
return socketResult.As<HighPerfUpdateSubscription>(null); return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, socketResult.Error);
socketConnection = socketResult.Data; socketConnection = socketResult.Data;
@@ -398,11 +405,9 @@ namespace CryptoExchange.Net.Clients
released = true; released = true;
} }
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false); var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
if (!connectResult) if (!connectResult.Success)
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!); return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, connectResult.Error!);
break; break;
} }
@@ -418,10 +423,10 @@ namespace CryptoExchange.Net.Clients
{ {
// Send the request and wait for answer // Send the request and wait for answer
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false); var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
if (!sendResult) if (!sendResult.Success)
{ {
await socketConnection.CloseAsync().ConfigureAwait(false); await socketConnection.CloseAsync().ConfigureAwait(false);
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!); return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, sendResult.Error!);
} }
} }
@@ -435,7 +440,13 @@ namespace CryptoExchange.Net.Clients
} }
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription)); return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
default,
default,
socketConnection.ConnectionUri.ToString(),
new HighPerfUpdateSubscription(socketConnection, subscription));
} }
/// <summary> /// <summary>
@@ -445,7 +456,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default) protected virtual Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
{ {
return QueryAsync(BaseAddress, query, ct); return QueryAsync(BaseAddress, query, ct);
} }
@@ -458,13 +469,13 @@ namespace CryptoExchange.Net.Clients
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default) protected virtual async Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
{ {
if (_disposing) if (_disposed)
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query")); return QueryResult.Fail<THandlerResponse>(Exchange, new InvalidOperationError("Client disposed, can't query"));
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError()); return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
@@ -472,8 +483,8 @@ namespace CryptoExchange.Net.Clients
try try
{ {
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult) if (!socketResult.Success)
return socketResult.As<THandlerResponse>(default); return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
socketConnection = socketResult.Data; socketConnection = socketResult.Data;
@@ -485,8 +496,8 @@ namespace CryptoExchange.Net.Clients
} }
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false); var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
if (!connectResult) if (!connectResult.Success)
return new CallResult<THandlerResponse>(connectResult.Error!); return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
} }
finally finally
{ {
@@ -497,11 +508,11 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); return QueryResult.Fail<THandlerResponse>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError()); return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false); return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
} }
@@ -516,23 +527,23 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct) protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
{ {
if (socket.Connected) if (socket.Connected)
return CallResult.SuccessResult; return CallResult.Ok();
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false); var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
if (!connectResult) if (!connectResult.Success)
return connectResult; return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero) if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false); await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated) if (!authenticated || socket.Authenticated)
return CallResult.SuccessResult; return CallResult.Ok();
if (socket is not SocketConnection sc) if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication"); throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false); var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
if (!result) if (!result.Success)
await socket.CloseAsync().ConfigureAwait(false); await socket.CloseAsync().ConfigureAwait(false);
return result; return result;
@@ -545,37 +556,37 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket) public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
{ {
if (AuthenticationProvider == null) if (GetAuthenticationProvider() == null)
return new CallResult(new NoApiCredentialsError()); return CallResult.Fail(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId); _logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false); var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null) if (authRequest != null)
{ {
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
if (!result.Success)
if (!result)
{ {
_logger.AuthenticationFailed(socket.SocketId); _logger.AuthenticationFailed(socket.SocketId);
if (socket.Connected) if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false); await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message; result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!; return CallResult.Fail(result.Error)!;
} }
_logger.Authenticated(socket.SocketId); _logger.Authenticated(socket.SocketId);
} }
socket.Authenticated = true; socket.Authenticated = true;
return CallResult.SuccessResult; return CallResult.Ok();
} }
/// <summary> /// <summary>
/// Should return the request which can be used to authenticate a socket connection /// Should return the request which can be used to authenticate a socket connection
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException(); protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
/// <summary> /// <summary>
/// Adds a system subscription. Used for example to reply to ping requests /// Adds a system subscription. Used for example to reply to ping requests
@@ -596,7 +607,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication) protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
{ {
return Task.FromResult(new CallResult<string?>(address)); return Task.FromResult(CallResult.Ok<string?>(address));
} }
/// <summary> /// <summary>
@@ -616,7 +627,22 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription) protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
{ {
return Task.FromResult(CallResult.SuccessResult); return Task.FromResult(CallResult.Ok());
}
/// <summary>
/// Whether the connection can be used for a new subscription or query with the provided parameters
/// </summary>
/// <param name="connection">The connection to check</param>
/// <param name="address">The address set by the request</param>
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
/// <param name="topic">Topic of the request</param>
/// <returns>True if connection can be used</returns>
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, string? topic = null)
{
return connection.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
&& connection.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
} }
/// <summary> /// <summary>
@@ -637,24 +663,20 @@ namespace CryptoExchange.Net.Clients
string? topic = null, string? topic = null,
int individualSubscriptionCount = 1) int individualSubscriptionCount = 1)
{ {
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
&& s.Value.ApiClient.GetType() == GetType()
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value)
.ToList();
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection // If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow; var delayStart = DateTime.UtcNow;
var delayed = false; 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 (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 // If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket(); _logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError()); return CallResult.Fail<SocketConnection>(new CantConnectError());
} }
break; break;
@@ -664,22 +686,19 @@ namespace CryptoExchange.Net.Clients
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { } try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError()); return CallResult.Fail<SocketConnection>(new CancellationRequestedError());
} }
if (delayed) if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds); _logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected) socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated) && (s.Authenticated == authenticated || !authenticated || UsePublicConnectionForAuth)
&& s.Connected).ToList(); && s.Connected).ToList();
SocketConnection? connection; bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
if (!dedicatedRequestConnection) SocketConnection? connection = null;
{ if (dedicatedRequestConnection)
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
else
{ {
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault(); connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated) if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
@@ -687,42 +706,34 @@ namespace CryptoExchange.Net.Clients
connection.DedicatedRequestConnection.Authenticated = authenticated; connection.DedicatedRequestConnection.Authenticated = authenticated;
} }
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections); if (connection == null)
{
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
}
if (connection != null) if (connection != null)
{ return CallResult.Ok(connection);
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|| maxConnectionsReached)
{
// 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<SocketConnection>(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return new CallResult<SocketConnection>(connection);
}
}
if (maxConnectionsReached) if (maxConnectionsReached)
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached")); return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress) if (!connectionAddress.Success)
{ {
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); _logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return connectionAddress.As<SocketConnection>(null); return CallResult.Fail<SocketConnection>(connectionAddress.Error);
} }
if (connectionAddress.Data != address) if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!); _logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection // 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.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync; socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection) if (dedicatedRequestConnection)
{ {
@@ -739,7 +750,7 @@ namespace CryptoExchange.Net.Clients
foreach (var systemSubscription in systemSubscriptions) foreach (var systemSubscription in systemSubscriptions)
socketConnection.AddSubscription(systemSubscription); socketConnection.AddSubscription(systemSubscription);
return new CallResult<SocketConnection>(socketConnection); return CallResult.Ok(socketConnection);
} }
@@ -756,31 +767,47 @@ namespace CryptoExchange.Net.Clients
CancellationToken ct) CancellationToken ct)
{ {
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false); var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
if (!connectionAddress) if (!connectionAddress.Success)
{ {
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); _logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null); return CallResult.Fail<HighPerfSocketConnection<TUpdateType>>(connectionAddress.Error);
} }
if (connectionAddress.Data != address) if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!); _logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection // Create new socket connection
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address); var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
foreach (var ptg in PeriodicTaskRegistrations) foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request); socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection); return CallResult.Ok(socketConnection);
} }
private bool IsConnectionEligible(SocketConnection socketConnection, int individualSubscriptionCount, bool maxConnectionsReached)
{
var currentIndividualSubscriptionCount = socketConnection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
bool lessThanBatchSubCombineTarget = socketConnection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
// Include the incoming batch so batched subscriptions cannot overshoot the configured socket target.
bool lessThanIndividualSubCombineTarget = currentIndividualSubscriptionCount + individualSubscriptionCount <= ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((!lessThanBatchSubCombineTarget || !lessThanIndividualSubCombineTarget)
&& !maxConnectionsReached)
{
return false;
}
return MaxIndividualSubscriptionsPerConnection == null
|| currentIndividualSubscriptionCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection;
}
/// <summary> /// <summary>
/// Process an unhandled message /// Process an unhandled message
/// </summary> /// </summary>
/// <param name="message">The message that wasn't processed</param> /// <param name="connection">The socket connection</param>
protected virtual void HandleUnhandledMessage(IMessageAccessor message) /// <param name="typeIdentifier">The type as identified</param>
{ /// <param name="data">The data</param>
} protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan<byte> data) => false;
/// <summary> /// <summary>
/// Process connect rate limited /// Process connect rate limited
@@ -805,7 +832,7 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct) protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
{ {
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false); var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult) if (connectResult.Success)
{ {
if (socketConnection is SocketConnection sc) if (socketConnection is SocketConnection sc)
_socketConnections.TryAdd(socketConnection.SocketId, sc); _socketConnections.TryAdd(socketConnection.SocketId, sc);
@@ -831,10 +858,11 @@ namespace CryptoExchange.Net.Clients
ReconnectInterval = ClientOptions.ReconnectInterval, ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null, RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour, RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
RateLimitAdmissionCallbackOptions = (def, weight) => ClientOptions.RateLimitAdmission?.Invoke(def, weight),
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
Proxy = ClientOptions.Proxy, Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout, Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize, ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
UseUpdatedDeserialization = ClientOptions.UseUpdatedDeserialization
}; };
/// <summary> /// <summary>
@@ -929,34 +957,15 @@ namespace CryptoExchange.Net.Clients
foreach (var item in DedicatedConnectionConfigs) foreach (var item in DedicatedConnectionConfigs)
{ {
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false); var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult) if (!socketResult.Success)
return socketResult.AsDataless(); return CallResult.Fail(socketResult.Error);
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false); var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
if (!connectResult) if (!connectResult.Success)
return new CallResult(connectResult.Error!); return CallResult.Fail(connectResult.Error!);
} }
return CallResult.SuccessResult; return CallResult.Ok();
}
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
} }
/// <summary> /// <summary>
@@ -1027,7 +1036,6 @@ namespace CryptoExchange.Net.Clients
sb.AppendLine($"\t\t\tId: {subState.Id}"); sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}"); sb.AppendLine($"\t\t\tStatus: {subState.Status}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}"); sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
}); });
} }
}); });
@@ -1039,9 +1047,13 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Dispose the client /// Dispose the client
/// </summary> /// </summary>
public override void Dispose() protected override void Dispose(bool disposing)
{ {
_disposing = true; if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
if (!disposing)
return;
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
@@ -1055,29 +1067,199 @@ namespace CryptoExchange.Net.Clients
} }
semaphoreSlim?.Dispose(); semaphoreSlim?.Dispose();
base.Dispose(); base.Dispose(disposing);
}
} }
/// <summary>
/// Get the listener identifier for the message
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
/// <summary> /// <summary>
/// Preprocess a stream message /// Preprocess a stream message
/// </summary> /// </summary>
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data; public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
/// <summary>
/// Preprocess a stream message
/// </summary>
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
/// <summary> /// <summary>
/// Create a new message converter instance /// Create a new message converter instance
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType); public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions options)
{
var previousProxyIsSet = _proxyConfigured;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
_proxyConfigured = options.Proxy != null;
if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
}
/// <inheritdoc />
public abstract class SocketApiClient<TEnvironment> : SocketApiClient, ISocketApiClient
where TEnvironment : TradeEnvironment
{
/// <inheritdoc />
public new SocketExchangeOptions<TEnvironment> ClientOptions => (SocketExchangeOptions<TEnvironment>)base.ClientOptions;
/// <inheritdoc />
public override string EnvironmentName => ClientOptions.Environment.Name;
/// <summary>
/// ctor
/// </summary>
protected SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment> options,
SocketApiOptions apiOptions) : base(
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
{
}
}
/// <inheritdoc />
public abstract class SocketApiClient<TEnvironment, TApiCredentials> : SocketApiClient<TEnvironment>, ISocketApiClient<TApiCredentials>
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
/// <inheritdoc />
public TApiCredentials? ApiCredentials { get; set; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
protected SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
{
ApiCredentials = options.ApiCredentials;
}
/// <inheritdoc />
public virtual void SetApiCredentials(TApiCredentials credentials)
{
ApiCredentials = (TApiCredentials)credentials.Copy();
}
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
base.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class SocketApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : SocketApiClient<TEnvironment, TApiCredentials>
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
/// <summary>
/// Auth provider initialized field
/// </summary>
protected bool _authProviderInitialized = false;
/// <summary>
/// Auth provider field
/// </summary>
protected TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public virtual TAuthenticationProvider? AuthenticationProvider
{
get
{
if (!_authProviderInitialized)
{
if (ApiCredentials != null)
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
_authProviderInitialized = true;
}
return _authenticationProvider;
}
protected internal set => _authenticationProvider = value;
}
/// <inheritdoc />
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
/// <summary>
/// ctor
/// </summary>
protected SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
{
}
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
/// <inheritdoc />
public override void SetApiCredentials(TApiCredentials credentials)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = credentials;
base.SetApiCredentials(credentials);
}
/// <inheritdoc />
public override void SetOptions(UpdateOptions<TApiCredentials> options)
{
if (options.ApiCredentials != null)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = options.ApiCredentials;
}
base.SetOptions(options);
}
} }
} }
@@ -0,0 +1,212 @@
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
{
/// <inheritdoc />
public abstract class UserClientProvider<TRestClient, TRestOptions, TCredentials, TEnvironment>
where TRestClient : IRestClient<TCredentials>
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
where TCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private ConcurrentDictionary<string, TRestClient> _restClients = new ConcurrentDictionary<string, TRestClient>();
private readonly IOptions<TRestOptions> _restOptions;
private readonly HttpClient _httpClient;
/// <summary>
/// Logger factory
/// </summary>
protected readonly ILoggerFactory? _loggerFactory;
/// <inheritdoc />
public abstract string ExchangeName { get; }
/// <summary>
/// ctor
/// </summary>
public UserClientProvider(
HttpClient? httpClient,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> restOptions)
{
_httpClient = httpClient ?? new HttpClient();
_httpClient.Timeout = restOptions.Value.RequestTimeout;
_loggerFactory = loggerFactory;
_restOptions = restOptions;
}
private IOptions<TRestOptions> SetRestEnvironment(IOptions<TRestOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newRestClientOptions = new TRestOptions();
options.Value.Set(newRestClientOptions);
newRestClientOptions.Environment = environment;
return Options.Create(newRestClientOptions);
}
/// <inheritdoc />
public virtual void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
{
CreateRestClient(userIdentifier, credentials, environment);
}
/// <inheritdoc />
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;
}
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;
}
/// <summary>
/// Constructs a new instance of the rest client
/// </summary>
protected abstract TRestClient ConstructRestClient(
HttpClient client,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> options);
/// <inheritdoc />
public virtual void ClearUserClients(string userIdentifier)
{
_restClients.TryRemove(userIdentifier, out var restClient);
restClient?.Dispose();
}
/// <inheritdoc />
public virtual void Clear()
{
foreach (var client in _restClients.Values)
client.Dispose();
_restClients.Clear();
}
/// <summary>
/// Applies the provided options delegate to a new instance of the specified type.
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T : new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
}
/// <inheritdoc />
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
: UserClientProvider<TRestClient, TRestOptions, TCredentials, TEnvironment>
where TRestClient : IRestClient<TCredentials>
where TSocketClient : ISocketClient<TCredentials>
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
where TSocketOptions : SocketExchangeOptions<TEnvironment, TCredentials>, new()
where TCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private ConcurrentDictionary<string, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
private readonly IOptions<TSocketOptions> _socketOptions;
/// <summary>
/// ctor
/// </summary>
public UserClientProvider(
HttpClient? httpClient,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> restOptions,
IOptions<TSocketOptions> socketOptions)
: base(httpClient, loggerFactory, restOptions)
{
_socketOptions = socketOptions;
}
private IOptions<TSocketOptions> SetSocketEnvironment(IOptions<TSocketOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newSocketClientOptions = new TSocketOptions();
options.Value.Set(newSocketClientOptions);
newSocketClientOptions.Environment = environment;
return Options.Create(newSocketClientOptions);
}
/// <inheritdoc />
public override void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
{
base.InitializeUserClient(userIdentifier, credentials, environment);
CreateSocketClient(userIdentifier, credentials, environment);
}
/// <inheritdoc />
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 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;
}
/// <summary>
/// Constructs a new instance of the socket client
/// </summary>
protected abstract TSocketClient ConstructSocketClient(
ILoggerFactory? loggerFactory,
IOptions<TSocketOptions> options);
/// <inheritdoc />
public override void ClearUserClients(string userIdentifier)
{
base.ClearUserClients(userIdentifier);
_socketClients.TryRemove(userIdentifier, out var socketClient);
socketClient?.Dispose();
}
/// <inheritdoc />
public override void Clear()
{
base.Clear();
foreach (var client in _socketClients.Values)
client.Dispose();
_socketClients.Clear();
}
}
}
@@ -1,49 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Node accessor
/// </summary>
public readonly struct NodeAccessor
{
/// <summary>
/// Index
/// </summary>
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{
Index = index;
Property = property;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
}
}
@@ -1,50 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message access definition
/// </summary>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{
private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -1,43 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
}
@@ -1,21 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{
/// <summary>
/// Array node
/// </summary>
Array,
/// <summary>
/// Object node
/// </summary>
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
}
@@ -173,6 +173,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value); attribute.PropertyInfo.SetValue(result, value);
else if (value is string stringValue && targetType == typeof(decimal))
// Convert.ChangeType doesn't accept scientific notation, which some exchanges use for small values ("4.803E-5")
attribute.PropertyInfo.SetValue(result, ExchangeHelpers.ParseDecimal(stringValue));
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
@@ -56,14 +56,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.False) if (reader.TokenType == JsonTokenType.False)
return false; return false;
var value = reader.TokenType switch if (reader.TokenType == JsonTokenType.Number)
{ {
JsonTokenType.String => reader.GetString(), var number = reader.GetInt16();
JsonTokenType.Number => reader.GetInt16().ToString(), if (number >= 1)
_ => null 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 (string.IsNullOrEmpty(value))
{ {
if (typeToConvert == typeof(bool)) if (typeToConvert == typeof(bool))
@@ -73,12 +85,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
switch (value) switch (value)
{ {
case "enabled":
case "true": case "true":
case "yes": case "yes":
case "y": case "y":
case "1": case "1":
case "on": case "on":
return true; return true;
case "disabled":
case "false": case "false":
case "no": case "no":
case "n": case "n":
@@ -88,7 +102,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false; return false;
} }
throw new SerializationException($"Can't convert bool value {value}"); throw new SerializationException($"Can't convert bool value, unknown string value: {value}");
} }
} }
@@ -0,0 +1,30 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated string values
/// </summary>
public class CommaSplitStringConverter : JsonConverter<string[]>
{
/// <inheritdoc />
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value));
}
}
}
@@ -16,17 +16,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m; private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
private static Type _dateTimeType = typeof(DateTime);
private static Type _nullableDateTimeType = typeof(DateTime?);
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type typeToConvert) public override bool CanConvert(Type typeToConvert)
{ {
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?); return typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType;
} }
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 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<DateTime?> private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
@@ -68,7 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
if (reader.TokenType == JsonTokenType.Null) 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); LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default; return default;
} }
@@ -76,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number) if (reader.TokenType is JsonTokenType.Number)
{ {
var decValue = reader.GetDecimal(); var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0) if (decValue <= 0)
return default; return default;
return ParseFromDecimal(decValue); return ParseFromDecimal(decValue);
@@ -86,8 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.GetString(); var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue) if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue!.Equals("-1", StringComparison.Ordinal) || stringValue!.Equals("-1", StringComparison.Ordinal)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase) || stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase))
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
{ {
return default; return default;
} }
@@ -124,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary> /// <summary>
/// Parse a string value to datetime /// Parse a string value to datetime
/// </summary> /// </summary>
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)) if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{ {
@@ -197,6 +198,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
} }
if (stringValue.EndsWith("+00:00:00"))
return DateTime.Parse(stringValue.Substring(0, stringValue.Length - 9), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
} }
@@ -67,28 +67,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif #endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum : JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{ {
class EnumMapping
{
public T Value { get; set; }
public string StringValue { get; set; }
public EnumMapping(T value, string stringValue)
{
Value = value;
StringValue = stringValue;
}
}
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
private static FrozenSet<EnumMapping>? _mappingToEnum = null; private static FrozenDictionary<string, T>? _mappingToEnum = null;
private static FrozenDictionary<T, string>? _mappingToString = null; private static FrozenDictionary<T, string>? _mappingToString = null;
private static bool RunOptimistic => true;
#else #else
private static List<EnumMapping>? _mappingToEnum = null; private static Dictionary<string, T>? _mappingToEnum = null;
private static Dictionary<T, string>? _mappingToString = null; private static Dictionary<T, string>? _mappingToString = null;
// In NetStandard the `ValueTextEquals` method used is slower than just string comparing
// so only bother in newer frameworks
private static bool RunOptimistic => false;
#endif #endif
private NullableEnumConverter? _nullableEnumConverter = null; private NullableEnumConverter? _nullableEnumConverter = null;
private static Type _enumType = typeof(T);
private static T? _undefinedEnumValue;
private static bool _hasFlagsAttribute = _enumType.IsDefined(typeof(FlagsAttribute));
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>(); private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
private static ConcurrentBag<string> _notOptimalValuesWarned = new ConcurrentBag<string>();
private const int _optimisticValueCountThreshold = 6;
internal class NullableEnumConverter : JsonConverter<T?> internal class NullableEnumConverter : JsonConverter<T?>
{ {
@@ -119,30 +120,51 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyStringOrNull);
if (t == null) if (t != null)
{ return t.Value;
if (isEmptyString && !_unknownValuesWarned.Contains(null))
if (isEmptyStringOrNull && !_unknownValuesWarned.Contains(null))
{ {
// We received an empty string and have no mapping for it, and the property isn't nullable // We received an empty string and have no mapping for it, and the property isn't nullable
_unknownValuesWarned.Add(null!);
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo"); LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
} }
return new T(); // return default value return GetUndefinedEnumValue();
}
else
{
return t.Value;
}
} }
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString) private T GetUndefinedEnumValue()
{ {
isEmptyString = false; if (_undefinedEnumValue != null)
var enumType = typeof(T); return _undefinedEnumValue.Value;
var type = typeof(T);
if (!Enum.IsDefined(type, -9))
_undefinedEnumValue = (T)Enum.ToObject(type, -9);
else if (!Enum.IsDefined(type, -99))
_undefinedEnumValue = (T)Enum.ToObject(type, -99);
else
_undefinedEnumValue = (T)Enum.ToObject(type, -999);
return (T)_undefinedEnumValue;
}
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
{
isEmptyStringOrNull = false;
if (_mappingToEnum == null) if (_mappingToEnum == null)
CreateMapping(); CreateMapping();
bool optimisticCheckDone = false;
if (RunOptimistic)
{
var resultOptimistic = GetValueOptimistic(ref reader, ref optimisticCheckDone);
if (resultOptimistic != null)
return resultOptimistic.Value;
}
var isNumber = reader.TokenType == JsonTokenType.Number;
var stringValue = reader.TokenType switch var stringValue = reader.TokenType switch
{ {
JsonTokenType.String => reader.GetString(), JsonTokenType.String => reader.GetString(),
@@ -154,13 +176,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}; };
if (stringValue is null) if (stringValue is null)
return null;
if (!GetValue(enumType, stringValue, out var result))
{ {
isEmptyStringOrNull = true;
return null;
}
if (!GetValue(stringValue, optimisticCheckDone, out var result))
{
// Note: checking this here and before the GetValue seems redundant but it allows enum mapping for empty strings
if (string.IsNullOrWhiteSpace(stringValue)) if (string.IsNullOrWhiteSpace(stringValue))
{ {
isEmptyString = true; isEmptyStringOrNull = true;
} }
else else
{ {
@@ -168,13 +194,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_unknownValuesWarned.Contains(stringValue)) if (!_unknownValuesWarned.Contains(stringValue))
{ {
_unknownValuesWarned.Add(stringValue!); _unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo"); LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.Key}: {m.Value}"))}]. If you think {stringValue} should be added please open an issue on the Github repo");
} }
} }
return null; return null;
} }
if (optimisticCheckDone)
{
if (!_notOptimalValuesWarned.Contains(stringValue))
{
_notOptimalValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogTrace($"Enum mapping sub-optimal. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.Key}: {m.Value}"))}]");
}
}
return result; return result;
} }
@@ -185,29 +220,62 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStringValue(stringValue); writer.WriteStringValue(stringValue);
} }
private static bool GetValue(Type objectType, string value, out T? result) /// <summary>
/// Try to get the enum value based on the string value using the Utf8JsonReader's ValueTextEquals method.
/// This is an optimization to avoid string allocations when possible, but can only match case sensitively
/// </summary>
private static T? GetValueOptimistic(ref Utf8JsonReader reader, ref bool optimisticCheckDone)
{ {
if (_mappingToEnum != null) if (reader.TokenType != JsonTokenType.String)
{
optimisticCheckDone = false;
return null;
}
if (_mappingToEnum!.Count >= _optimisticValueCountThreshold)
{
optimisticCheckDone = false;
return null;
}
optimisticCheckDone = true;
foreach (var item in _mappingToEnum!)
{
if (reader.ValueTextEquals(item.Key))
return item.Value;
}
return null;
}
private static bool GetValue(string value, bool optimisticCheckDone, out T? result)
{
if (_mappingToEnum == null)
throw new InvalidOperationException("Enum mapping not initialized");
T? mapping = null;
// If we tried the optimistic path first we already know its not case match
if (!optimisticCheckDone)
{ {
EnumMapping? mapping = null;
// Try match on full equals // Try match on full equals
foreach (var item in _mappingToEnum) foreach (var item in _mappingToEnum)
{ {
if (item.StringValue.Equals(value, StringComparison.Ordinal)) if (item.Key.Equals(value, StringComparison.Ordinal))
{ {
mapping = item; mapping = item.Value;
break; break;
} }
} }
}
// If not found, try matching ignoring case // If not found, try matching ignoring case
if (mapping == null) if (mapping == null)
{ {
foreach (var item in _mappingToEnum) foreach (var item in _mappingToEnum)
{ {
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase)) if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
{ {
mapping = item; mapping = item.Value;
break; break;
} }
} }
@@ -215,15 +283,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (mapping != null) if (mapping != null)
{ {
result = mapping.Value; result = mapping;
return true; return true;
} }
}
if (objectType.IsDefined(typeof(FlagsAttribute)))
if (_hasFlagsAttribute)
{ {
var intValue = int.Parse(value); var intValue = int.Parse(value);
result = (T)Enum.ToObject(objectType, intValue); result = (T)Enum.ToObject(_enumType, intValue);
return true; return true;
} }
@@ -245,7 +313,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
// If no explicit mapping is found try to parse string // If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true); #if NET8_0_OR_GREATER
result = Enum.Parse<T>(value, true);
#else
result = (T)Enum.Parse(_enumType, value, true);
#endif
if (!Enum.IsDefined(_enumType, result))
{
result = default;
return false;
}
return true; return true;
} }
catch (Exception) catch (Exception)
@@ -257,35 +335,43 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static void CreateMapping() private static void CreateMapping()
{ {
var mappingToEnum = new List<EnumMapping>(); var mappingStringToEnum = new Dictionary<string, T>();
var mappingToString = new Dictionary<T, string>(); var mappingEnumToString = new Dictionary<T, string>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); #pragma warning disable IL2080
var enumMembers = enumType.GetFields(); var enumMembers = _enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
#pragma warning restore IL2080
foreach (var member in enumMembers) foreach (var member in enumMembers)
{ {
var enumVal = (T)member.GetValue(null)!;
var maps = member.GetCustomAttributes(typeof(MapAttribute), false); var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps) foreach (MapAttribute attribute in maps)
{ {
foreach (var value in attribute.Values) foreach (var value in attribute.Values)
{ {
var enumVal = (T)Enum.Parse(enumType, member.Name); mappingStringToEnum.Add(value, enumVal);
mappingToEnum.Add(new EnumMapping(enumVal, value)); if (!mappingEnumToString.ContainsKey(enumVal))
if (!mappingToString.ContainsKey(enumVal)) mappingEnumToString.Add(enumVal, value);
mappingToString.Add(enumVal, value);
} }
} }
} }
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
_mappingToEnum = mappingToEnum.ToFrozenSet(); _mappingToEnum = mappingStringToEnum.ToFrozenDictionary();
_mappingToString = mappingToString.ToFrozenDictionary(); _mappingToString = mappingEnumToString.ToFrozenDictionary();
#else #else
_mappingToEnum = mappingToEnum; _mappingToEnum = mappingStringToEnum;
_mappingToString = mappingToString; _mappingToString = mappingEnumToString;
#endif #endif
} }
// For testing purposes only, allows resetting the static mapping and warnings
internal static void Reset()
{
_undefinedEnumValue = null;
_unknownValuesWarned = new ConcurrentBag<string>();
}
/// <summary> /// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary> /// </summary>
@@ -307,41 +393,30 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <returns></returns> /// <returns></returns>
public static T? ParseString(string value) public static T? ParseString(string value)
{ {
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mappingToEnum == null) if (_mappingToEnum == null)
CreateMapping(); CreateMapping();
EnumMapping? mapping = null;
// Try match on full equals // Try match on full equals
foreach(var item in _mappingToEnum!) foreach(var item in _mappingToEnum!)
{ {
if (item.StringValue.Equals(value, StringComparison.Ordinal)) if (item.Key.Equals(value, StringComparison.Ordinal))
{ return item.Value;
mapping = item;
break;
}
} }
// If not found, try matching ignoring case // If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum) foreach (var item in _mappingToEnum)
{ {
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase)) if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
{ return item.Value;
mapping = item;
break;
} }
}
}
if (mapping != null)
return mapping.Value;
try try
{ {
// If no explicit mapping is found try to parse string #if NET8_0_OR_GREATER
return (T)Enum.Parse(type, value, true); return Enum.Parse<T>(value, true);
#else
return (T)Enum.Parse(_enumType, value, true);
#endif
} }
catch (Exception) catch (Exception)
{ {
@@ -0,0 +1,38 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Bool converter
/// </summary>
public class IntBoolConverter : JsonConverter<bool>
{
private readonly int _trueValue;
/// <summary>
/// ctor
/// </summary>
/// <param name="trueValue">The int value representing the true value</param>
public IntBoolConverter(int trueValue)
{
_trueValue = trueValue;
}
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
return false;
return reader.GetDecimal() == _trueValue;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteNumberValue(_trueValue);
}
}
}
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
public abstract class JsonRestMessageHandler : IRestMessageHandler public abstract class JsonRestMessageHandler : IRestMessageHandler
{ {
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader); private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
private const int _errorResponseSnippetLimit = 128;
/// <summary> /// <summary>
/// Empty rate limit error /// Empty rate limit error
@@ -80,7 +81,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
} }
catch (Exception ex) catch (Exception ex)
{ {
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null); var errorMsg = "Deserialization failed, invalid JSON";
if (stream.CanSeek)
{
var dataSnippet = new char[_errorResponseSnippetLimit];
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
var error = new DeserializeError(errorMsg, ex);
return (error, null);
} }
} }
@@ -165,6 +165,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
return null; return null;
} }
/// <summary>
/// Return type identifier for non-json messages
/// </summary>
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
return null;
}
/// <inheritdoc /> /// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType) public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{ {
@@ -173,6 +181,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
int? arrayIndex = null; int? arrayIndex = null;
_searchResult.Clear(); _searchResult.Clear();
if (data[0] != 0x5B && data[0] != 0x7B)
{
// Message doesn't start with `{` or `[`, not valid for processing as json
return GetTypeIdentifierNonJson(data, webSocketMessageType);
}
var reader = new Utf8JsonReader(data); var reader = new Utf8JsonReader(data);
while (reader.Read()) while (reader.Read())
{ {
@@ -0,0 +1,58 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for parsing object or array responses
/// </summary>
public class ObjectOrArrayConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(type)!;
}
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
{
// Object to object
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
{
// Array to array
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
// Array to object
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
return default;
}
else
{
// Object to array
JsonDocument.ParseValue(ref reader);
return default;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}
}
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
return default; return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options); return JsonDocument.Parse(value!).Deserialize<T>(options);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // Start array reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal(); var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
reader.Read(); reader.Read();
if (reader.TokenType != JsonTokenType.EndArray) if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // End array reader.Read(); // End array
@@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // Start array reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!); var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
reader.Read(); reader.Read();
if (reader.TokenType != JsonTokenType.EndArray) if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // End array reader.Read(); // End array
@@ -1,373 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
{
/// <summary>
/// The JsonDocument loaded
/// </summary>
protected JsonDocument? _document;
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsValid)
return new CallResult<object>(GetOriginalString());
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
return _document.RootElement.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (!node.HasValue)
return null;
return node.Value.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions);
}
catch { }
return default;
}
if (typeof(T) == typeof(string))
{
if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>(_customSerializerOptions);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
}
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
JsonElement? currentToken = _document.RootElement;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
return null;
currentToken = currentToken.Value[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
return null;
currentToken = token;
}
else
{
// Property name
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
throw new NotImplementedException();
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_document?.Dispose();
_document = null;
}
}
/// <summary>
/// System.Text.Json byte message accessor
/// </summary>
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
var firstByte = data.Span[0];
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false;
return new CallResult(new DeserializeError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_document?.Dispose();
_document = null;
}
}
}
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>10.0.1</PackageVersion> <PackageVersion>13.0.0</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion> <AssemblyVersion>13.0.0</AssemblyVersion>
<FileVersion>10.0.1</FileVersion> <FileVersion>13.0.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
+106 -12
View File
@@ -4,6 +4,7 @@ using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading; using System.Threading;
@@ -242,8 +243,7 @@ namespace CryptoExchange.Net
/// <summary> /// <summary>
/// Generate a long value /// Generate a long value
/// </summary> /// </summary>
/// <param name="maxLength">Max character length</param> /// <param name="maxLength">Max number of digits</param>
/// <returns></returns>
public static long RandomLong(int maxLength) public static long RandomLong(int maxLength)
{ {
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
@@ -259,6 +259,25 @@ namespace CryptoExchange.Net
return value; return value;
} }
/// <summary>
/// Generate a long value between two values
/// </summary>
/// <param name="minValue">Min value</param>
/// <param name="maxValue">Max value</param>
/// <returns></returns>
public static long RandomLong(long minValue, long maxValue)
{
#if NET8_0_OR_GREATER
var buf = RandomNumberGenerator.GetBytes(8);
#else
byte[] buf = new byte[8];
var random = new Random();
random.NextBytes(buf);
#endif
long longRand = BitConverter.ToInt64(buf, 0);
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
}
/// <summary> /// <summary>
/// Generate a random string of specified length /// Generate a random string of specified length
/// </summary> /// </summary>
@@ -292,25 +311,55 @@ namespace CryptoExchange.Net
/// <param name="request">The request parameters</param> /// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) public static async IAsyncEnumerable<HttpResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<HttpResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{ {
var result = new List<T>(); var result = new List<T>();
ExchangeWebResult<T[]> batch; HttpResult<T[]> batch;
INextPageToken? nextPageToken = null; PageRequest? nextPageToken = null;
while (true) while (true)
{ {
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false); batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch; yield return batch;
if (!batch || ct.IsCancellationRequested) if (!batch.Success || ct.IsCancellationRequested)
break; break;
result.AddRange(batch.Data); result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken; nextPageToken = batch.NextPageRequest;
if (nextPageToken == null) if (nextPageToken == null)
break; break;
} }
} }
/// <summary>
/// Apply filters to the data set
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="data">Data set</param>
/// <param name="timeSelector">Time selector for the data</param>
/// <param name="startTime">Start time filter</param>
/// <param name="endTime">End time filter</param>
/// <param name="direction">Data direction</param>
public static IEnumerable<T> ApplyFilter<T>(
IEnumerable<T> data,
Func<T, DateTime> timeSelector,
DateTime? startTime,
DateTime? endTime,
DataDirection direction)
{
if (direction == DataDirection.Ascending)
data = data.OrderBy(timeSelector);
else
data = data.OrderByDescending(timeSelector);
if (startTime != null)
data = data.Where(x => timeSelector(x) >= startTime.Value);
if (endTime != null)
data = data.Where(x => timeSelector(x) < endTime.Value);
return data;
}
/// <summary> /// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price /// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary> /// </summary>
@@ -350,8 +399,8 @@ namespace CryptoExchange.Net
/// <param name="asyncHandler">The async update handler</param> /// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">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 <see>fullMode</see></param> /// <param name="maxQueuedItems">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 <see>fullMode</see></param>
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param> /// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>( public static async Task<WebSocketResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall, Func<Action<DataEvent<T>>, Task<WebSocketResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<T>, Task> asyncHandler, Func<DataEvent<T>, Task> asyncHandler,
int? maxQueuedItems = null, int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null) QueueFullBehavior? fullBehavior = null)
@@ -359,7 +408,7 @@ namespace CryptoExchange.Net
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior); var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false); await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false); var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
if (!result) if (!result.Success)
{ {
await processor.StopAsync().ConfigureAwait(false); await processor.StopAsync().ConfigureAwait(false);
return result; return result;
@@ -424,7 +473,7 @@ namespace CryptoExchange.Net
}, maxQueuedItems, fullBehavior); }, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false); await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(processor).ConfigureAwait(false); var result = await subscribeCall(processor).ConfigureAwait(false);
if (!result) if (!result.Success)
{ {
await processor.StopAsync().ConfigureAwait(false); await processor.StopAsync().ConfigureAwait(false);
return result; return result;
@@ -450,7 +499,7 @@ namespace CryptoExchange.Net
return null; return null;
// Try parse, only fails for these reasons: // 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 // 2. value is larger or smaller than decimal max/min
// 3. unparsable format // 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue)) if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
@@ -482,5 +531,50 @@ namespace CryptoExchange.Net
// Unknown decimal format, return null // Unknown decimal format, return null
return null; return null;
} }
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
public static string BytesToHexString(byte[] buff)
=> BytesToHexString(new ArraySegment<byte>(buff));
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
public static string BytesToHexString(ArraySegment<byte> buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2");
return result;
#endif
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
public static byte[] HexToBytesString(string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
} }
} }

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