1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-12-29 18:17:45 +00:00

Compare commits

...

571 Commits

Author SHA1 Message Date
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
Jkorf
beda53d36d Updated to version 10.0.1 2025-12-18 10:56:10 +01:00
Jkorf
0668f669c1 Fixed query parameter array serialization 2025-12-18 10:36:22 +01:00
Jkorf
64250e13db Updated examples 2025-12-17 10:55:48 +01:00
Jkorf
451d38d5e7 Updated to version 10.0.1 2025-12-16 11:54:37 +01:00
Jkorf
e11e437bbb Fixed CryptoExchange.Net reference 2025-12-16 11:54:08 +01:00
Jkorf
b8a1ad798d Updated to version 10.0.0 2025-12-16 11:49:45 +01:00
Jkorf
4a851c44f2 Updated to version 10.0.0 2025-12-16 11:31:42 +01:00
Jan Korf
d079796020
Websocket performance update (#261)
Performance update:

Authentication
	Added Ed25519 signing support for NET8.0 and newer
	Added static methods on ApiCredentials to create credentials of a specific type
	Added static ApiCredentials.ReadFromFile method to read a key from file
	Added required abstract SupportedCredentialTypes property on AuthenticationProvider base class

General Performance
	Added checks before logging statements to prevent overhead of building the log string if logging is not needed	
	Added ExchangeHelpers.ProcessQueuedAsync method to process updates async
	Replaced locking object types from object to Lock in NET9.0 and newer 
	Replaced some Task response types with ValueTask to prevent allocation overhead on hot paths
	Updated Json ArrayConverter to reduce some allocation overhead 
	Updated Json BoolConverter to prevent boxing
	Updated Json DateTimeConverter to prevent boxing
	Updated Json EnumConverter caching to reduce lookup overhead
	Updated ExtensionMethods.CreateParamString to reduce allocations
	Updated ExtensionMethods.AppendPath to reduce overhead	

REST 
	Refactored REST message processing to separate IRestMessageHandler instance
	Split RestApiClient.PrepareAsync into CheckTimeSync and RateLimitAsync
	Updated IRequest.Accept type from string to MediaTypeWithQualityHeaderValue to prevent creation on each request
	Updated IRequest.GetHeaders response type from KeyValuePair<string, string[]>[] to HttpRequestHeaders to prevent additional mapping
	Updated IResponse.ResponseHeaders type from KeyValuePair<string, string[]>[] to HttpResponseHeaders to prevent additional mapping
	Updated WebCallResult RequestHeaders and ResponseHeaders types to HttpRequestHeaders and HttpResponseHeaders	
	Removed unnecessary empty dictionary initializations for each request
	Removed CallResult creation in internal methods to prevent having to create multiple versions for different result types 

Socket
	Added HighPerformance websocket client implementation which significantly reduces memory overhead and improves speed but with certain limitations
	Added MaxIndividualSubscriptionsPerConnection setting in SocketApiClient to limit the number of individual stream subscriptions on a connection
	Added SocketIndividualSubscriptionCombineTarget option to set the target number of individual stream subscriptions per connection
	Added new websocket message handling logic which is faster and reduces memory allocation
	Added UseUpdatedDeserialization option to toggle between updated deserialization and old deserialization 
	Added Exchange property to DataEvent to prevent additional mapping overhead for Shared apis
	Refactored message callback to be sync instead of async to prevent async overhead
	Refactored CryptoExchangeWebSocketClient.IncomingKbps calculation to significantly reduce overhead
	Moved websocket client creation from SocketApiClient to SocketConnection	
	Removed DataEvent.As and DataEvent.ToCallResult methods in favor of single ToType method
	Removed DataEvent creation on lower levels to prevent having to create multiple versions for different result types
	Removed Subscription<TSubResponse, TUnsubResponse> as its no longer used

Other
	Added null check to ParameterCollection for required parameters 
	Added Net10.0 target framework
	Updated dependency versions
	Updated Shared asset aliases check to be culture invariant
	Updated Error string representation
	Updated some namespaces
	Updated SymbolOrderBook processing of buffered updates to prevent additional allocation
	Removed ExchangeEvent type which is no longer needed
	Removed unused usings
2025-12-16 11:27:49 +01:00
Jonnern
f125bc88b0
Pass CancellationToken to Content.ReadAsStreamAsync (#262) 2025-12-15 16:33:06 +01:00
Jkorf
f3d535f286 Fixed incorrect check for TimeFilterSupport in combination with StartTime parameter for some Shared endpoints 2025-11-20 14:03:59 +01:00
Jkorf
6e8c6feec2 Added dotnet 10 target framework, updated package reference versions 2025-11-13 10:10:55 +01:00
Jkorf
84b0444caf Added implementation for async processing of (websocket) updates 2025-11-12 11:19:40 +01:00
Jkorf
4be986ebe7 Added resolver name to datetime/bool parser warnings 2025-11-11 08:57:57 +01:00
Jkorf
21872f818a Updated to version 9.13.0 2025-11-10 13:30:33 +01:00
Jkorf
4e7c45d758 Updated CryptoExchange.Net to version 9.13.0 2025-11-10 13:27:44 +01:00
Jkorf
0e55e5f065 Updated to version 9.13.0 2025-11-10 13:15:48 +01:00
Jkorf
b1c5cf318a Cleanup 2025-11-10 13:12:16 +01:00
Jkorf
9d94a24862 Added IExchangeService interface 2025-11-10 09:15:29 +01:00
Jkorf
6cf9684f55 Added SharedTickerType for defining time used for ticker calculations by the API 2025-11-10 09:15:20 +01:00
Jkorf
8043a48c49 Fixed incorrect exchange name in static logger when using multiple libraries 2025-11-10 09:14:14 +01:00
Jkorf
7d657dd533 Updated examples 2025-11-03 15:29:19 +01:00
Jkorf
1bfdec1484 Added SharedSymbolModel base class to SharedFuturesKline, SharedKline, SharedTrade models 2025-11-03 11:25:10 +01:00
Jkorf
8d5b6a53f3 Updated to version 9.12.0 2025-11-03 10:39:05 +01:00
Jkorf
ad97102e7c Updated CryptoExchange.Net version 2025-11-03 10:38:53 +01:00
Jkorf
d8dc121386 Updated to version 9.12.0 2025-11-03 09:48:29 +01:00
Jkorf
10c3868c00 Revert "Updated to version 9.11.0"
This reverts commit 411ba00a82d42d833df36648b88ab7b6104d5437.
2025-11-03 09:46:55 +01:00
Jkorf
411ba00a82 Updated to version 9.11.0 2025-11-03 09:45:41 +01:00
JKorf
c1b2b62dbc Added AliasType to specify only one way conversion for AssetAliases 2025-11-02 15:17:44 +01:00
JKorf
3960cab7a7 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-11-02 11:41:30 +01:00
JKorf
34e9447e55 Added constant for selecting a USD asset for use in a Shared API/SharedSymbol 2025-11-02 11:41:26 +01:00
Jkorf
9d3295acc7 Removed some unhelpful verbose logs 2025-10-31 15:08:14 +01:00
Jkorf
995cd3d84c Updated to version 9.11.1 2025-10-30 14:33:26 +01:00
Jkorf
919cdf0075 Updated CryptoExchange.Net version 2025-10-30 14:32:41 +01:00
Jkorf
d181c9cfc1 Updated to version 9.11.0 2025-10-30 14:21:30 +01:00
Jkorf
5943142c44 Updated to version 9.11.0 2025-10-30 13:47:48 +01:00
Jkorf
dbc430e838 Added StaticLogger to LibraryHelpers, updated warning logging for converters to use StaticLogger 2025-10-30 12:52:05 +01:00
Jkorf
7413d03d31 Added client reference helper to LibraryHelpers 2025-10-30 11:25:27 +01:00
Jkorf
dd60067684 Updated examples 2025-10-27 12:00:51 +01:00
Jkorf
04e4ddf525 Fixed exception when initial trade snapshot has no items in TradeTracker 2025-10-27 11:59:54 +01:00
Jkorf
99bf6d7c75 Added Upbit reference 2025-10-27 11:44:12 +01:00
Jkorf
99a203933c Added missing release notes 2025-10-15 13:59:10 +02:00
Jkorf
b43d2a2040 Updated to version 9.10.0 2025-10-15 13:36:33 +02:00
Jkorf
ba9c406def Updated CryptoExchange.Net version 2025-10-15 13:34:50 +02:00
Jkorf
f5f4d50cc9 Updated to version 9.10.0 2025-10-15 13:24:25 +02:00
Jkorf
f87506b490 Added ITransferRestClient, updated Shared IBalanceRestClient to use SharedAccountType 2025-10-15 13:21:00 +02:00
Jkorf
f6f9a53ce5 Added ClientOrderId property to SharedUserTrade model 2025-10-13 15:42:27 +02:00
Jkorf
61130ef54e Added long overloads for parse methods in DateTimeConverter 2025-10-13 11:15:58 +02:00
Jkorf
e8bcbd59be Updated DateTimeConverter to work primarily with decimal values instead of doubles to fix some floating point parsing issues 2025-10-13 09:06:14 +02:00
Jkorf
d433ff7475 Updated to version 9.9.0 2025-10-06 13:47:21 +02:00
Jkorf
71957037d0 Updated CryptoExchange.Net version to 9.8.0 2025-10-06 13:45:47 +02:00
Jkorf
bcdcdbbd4e Updated to version 9.9.0 2025-10-06 13:26:23 +02:00
Jkorf
1ece13f5bc Updated socket Subscription status handling, fixing timing issue for connection events and adding SubscriptionStatusChanged event 2025-10-06 13:22:40 +02:00
Jkorf
da70ba6ec7 Added Aster reference 2025-10-06 10:37:28 +02:00
Jkorf
a832f0e4d4 Updated to version 9.8.0 2025-09-30 12:06:14 +02:00
Jkorf
6ab4d005a0 Updated CryptoExchange.Net version to 9.8.0 2025-09-30 12:04:28 +02:00
Jkorf
18dc935038 Updated to version 9.8.0 2025-09-30 11:56:00 +02:00
Jkorf
bb3a534f75 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-09-30 11:50:16 +02:00
nils2525
649ba370c6
Fixed EnumConverter to allow mapping empty string values (#253) 2025-09-30 11:49:58 +02:00
Jkorf
51732c5ce6 Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect 2025-09-29 14:43:39 +02:00
Jkorf
0ba7b46680 Added ContractAddress to SharedAsset model 2025-09-29 13:51:59 +02:00
Jkorf
94dfbb7b9e Fixed ExchangeHelpers.AdjustValueStep high precision calculation 2025-09-29 13:51:46 +02:00
Jkorf
b8b7512b35 Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed 2025-09-29 10:07:28 +02:00
Jkorf
aba6b773ce Added ITrackerFactory interface 2025-09-17 10:55:48 +02:00
Jkorf
d88fb0d356 Added BloFin to ReadMe and examples 2025-09-17 10:01:17 +02:00
Jkorf
b8c6d55156 CryptoManager.Net reference 2025-09-02 11:43:44 +02:00
Jkorf
d9a5481db2 Updated to version 9.7.0 2025-09-01 13:37:16 +02:00
Jkorf
6a8bb42c0e Updated CryptoExchange.Net for CryptoExchange.Net.Protobuf version to 9.7.0 2025-09-01 13:35:24 +02:00
Jkorf
2445f001ab Updated to version 9.7.0 2025-09-01 13:18:16 +02:00
Jkorf
c84fa9ac32 Fixed test 2025-09-01 13:16:20 +02:00
Jkorf
d44a11c44e HttpVersion update
Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
Added REST client option for selecting HTTP protocol version
Added REST client option for HTTP client keep alive interval
Added HttpVersion to WebCallResult responses
Updated request logic to default to using HTTP version 2.0 for dotnet core
2025-09-01 10:12:59 +02:00
Jkorf
b215cccda4 Updated to version 9.6.0 2025-08-25 10:17:35 +02:00
Jkorf
3eda488361 Updated CryptoExchange.Net.Protobuf to CryptoExchange.Net version 9.5.0 2025-08-25 10:15:14 +02:00
Jkorf
993a44de35 Updated to version 9.6.0 2025-08-25 10:03:17 +02:00
Jkorf
99465f99a1 Fixed test 2025-08-25 10:00:42 +02:00
Jkorf
d42de1fe90 Added support for parsing REST response even though status indicates error 2025-08-25 09:58:03 +02:00
Jkorf
d0284c62c0 Removed obsolete attribute on Error.Code property, updated the description 2025-08-22 16:12:01 +02:00
Jkorf
d92f3b7904 Added better support for subscriptions without subscribe confirmation 2025-08-22 10:17:16 +02:00
Jkorf
3e1b5ada69 Added check in websocket for receiving 401 unauthorized http response status when 101 was expected 2025-08-21 13:39:07 +02:00
Jkorf
6156fb8154 Fixed test 2025-08-19 14:42:03 +02:00
Jkorf
f2753aed1e Updated to version 9.5.0 2025-08-19 10:21:24 +02:00
Jkorf
e33d826381 Updated CryptoExchange.Net version to 9.5.0 2025-08-19 10:20:11 +02:00
Jkorf
364aa4d324 Updated to version 9.5.0 2025-08-19 10:07:07 +02:00
Jkorf
455b332757 Updated test methods 2025-08-19 10:05:14 +02:00
Jkorf
876b895645 Fixed warning 2025-08-19 10:01:49 +02:00
Jkorf
daf7ed9fe6 Refactored RestApiClient authentication to prevent duplicate query string / body serialization 2025-08-19 09:50:10 +02:00
Jan Korf
3e365f83c9
Error handling update 2025-08-18 11:03:26 +02:00
Jkorf
40977ebdbe Fixed timing issue in query response processing 2025-08-07 14:15:18 +02:00
Jkorf
4a9058fc1c Fix response type in websocket queries not interested in the response 2025-08-07 14:15:00 +02:00
JKorf
dab9a21608 Fixed IOrderBookSocketClient Shared interface not getting registered in DI 2025-08-04 17:12:34 +02:00
Jkorf
a89c222399 Updated to version 9.4.0 2025-08-04 09:56:45 +02:00
Jkorf
1e356d2a45 Updated CryptoExchange.Net and protobuf-net package versions for CryptoExchange.Net.Protobuf 2025-08-04 09:54:15 +02:00
Jkorf
eed794c2cf Updated to version 9.4.0 2025-08-04 09:47:36 +02:00
Jkorf
2f82e2015b Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported 2025-08-04 09:39:30 +02:00
Jkorf
ad599badb2 Added CryptoExchange.Net tag 2025-07-29 16:02:38 +02:00
Jkorf
1e45c73f1d Added CoinW to examples 2025-07-29 15:57:02 +02:00
Jkorf
49c1fda2c1 Added CoinW reference 2025-07-29 11:44:22 +02:00
Jkorf
32a31e464b Updated to version 9.3.1 2025-07-29 09:42:53 +02:00
Jkorf
cddb4167e4 Added Id property to SharedPosition model 2025-07-28 15:58:36 +02:00
Jkorf
65457d8df2 Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum 2025-07-28 15:58:19 +02:00
Jkorf
122a6cad43 Updated to version 9.3.0 2025-07-23 10:52:40 +02:00
Jkorf
4c0e841425 Updated CryptoExchange.Net version 2025-07-23 10:51:17 +02:00
Jkorf
92f5839aec Updated to version 9.3.0 2025-07-23 10:37:52 +02:00
Jan Korf
30475dae67
Feature/websocket listener update (#244)
Updated websocket message to listener matching logic to be more flexible
2025-07-23 10:31:03 +02:00
Jkorf
3d942bd503 Updated decimal parser to support "NaN" and "-Infinity" strings, added check for negative overflow value, improved performance in most cases 2025-07-21 09:42:52 +02:00
Jkorf
f739520e52 Updated to version 9.2.1 2025-07-16 10:51:41 +02:00
Jkorf
0152603ddb Added setting for whether or not to process unparsable websocket messages 2025-07-16 10:42:38 +02:00
Jkorf
aa06e0eead Fixed issue causing duplicate subscriptions and data in the TradeTracker and KlineTracker when websocket connection was reconnected 2025-07-16 09:27:42 +02:00
Jkorf
2fde9a285e Update README.md 2025-07-14 13:31:47 +02:00
Jkorf
b9f6eb6abb Update README.md 2025-07-14 13:25:01 +02:00
Jkorf
d77c4354a6 Updated to version 9.2.0 2025-07-14 11:42:40 +02:00
Jkorf
21860ddf85 Updated CryptoExchange.Net reference for CryptoExchange.Protobuf.Net 2025-07-14 11:33:30 +02:00
Jkorf
2cffa22cc2 Updated to version 9.2.0 2025-07-14 11:06:30 +02:00
EricGarnier
985ba9bb29
Simplify ArrayConverter (#241)
Co-authored-by: Eric GARNIER <ega@softfluent.com>
2025-07-14 10:57:48 +02:00
Jan Korf
96f23f163d
Feature/protobuf (#243)
Protobuf implementation
2025-07-14 10:56:18 +02:00
Jkorf
0e7d49991a Updated examples, added Toobit reference 2025-06-11 14:33:05 +02:00
Jkorf
3e635cf0fe Updated to version 9.1.0 2025-05-28 15:03:32 +02:00
Jkorf
1425c66c69 Dependencies update
* Updated dotnet package versions from 9.0.0 to 9.0.5
* Replaced Microsoft.Extensions.Logging.Abstractions with icrosoft.Extensions.Logging
* Replaced Microsoft.Extensions.Options.ConfigurationExtensions with Microsoft.Extensions.Configuration.Binder, which includes a source generator for AOT publishing
* Removed redundant Microsoft.Extensions.DependencyInjection.Abstractions package reference
2025-05-28 14:50:23 +02:00
Jkorf
fc3b7cc75b Added JsonConverter implementation for SharedQuantity and SharedSymbol types 2025-05-28 14:26:55 +02:00
Jkorf
2cc2dc6ceb Updated to version 9.0.1 2025-05-20 14:51:19 +02:00
Jkorf
7da8cedf66 Improved response time on CancellationToken cancel during subscribing 2025-05-20 14:49:43 +02:00
Jkorf
2cf10668dd Added support for sending query without expecting a response 2025-05-19 16:06:45 +02:00
Jkorf
f1342b5ff2 Examples 2025-05-14 14:30:05 +02:00
JKorf
a04b636a11 Disable AOT warnings in testing code 2025-05-13 20:03:34 +02:00
Jkorf
e4637ad295 Disable warning for AOT in testing helpers 2025-05-13 15:53:13 +02:00
Jkorf
3a1e43dabe Fixed framework check for setting IsAotCompatible project flag 2025-05-13 15:36:16 +02:00
Jkorf
10da1a7bfe Fix documentation link 2025-05-13 15:10:15 +02:00
Jkorf
37320ca862 Docs 2025-05-13 15:08:30 +02:00
Jkorf
2074a5e26f Updated to version 9.0.0 2025-05-13 10:28:12 +02:00
Jan Korf
6b14cdbf06
Feature/9.0.0 (#236)
* Added support for Native AOT compilation
* Updated all IEnumerable response types to array response types
* Added Pass support for ApiCredentials, removing the need for most implementations to add their own ApiCredentials type
* Added KeepAliveTimeout setting setting ping frame timeouts for SocketApiClient
* Added IBookTickerRestClient Shared interface for requesting book tickers
* Added ISpotTriggerOrderRestClient Shared interface for managing spot trigger orders
* Added ISpotOrderClientIdClient Shared interface for managing spot orders by client order id
* Added IFuturesTriggerOrderRestClient Shared interface for managing futures trigger orders
* Added IFuturesOrderClientIdClient Shared interface for managing futures orders by client order id
* Added IFuturesTpSlRestClient Shared interface for setting TP/SL on open futures positions
* Added GenerateClientOrderId to ISpotOrderRestClient and IFuturesOrderRestClient interface
* Added OptionalExchangeParameters and Supported properties to EndpointOptions
* Refactor Shared interfaces quantity parameters and properties to use SharedQuantity
* Added SharedSymbol property to Shared interface models returning a symbol
* Added TriggerPrice, IsTriggerOrder, TakeProfitPrice, StopLossPrice and IsCloseOrder to SharedFuturesOrder response model
* Added MaxShortLeverage and MaxLongLeverage to SharedFuturesSymbol response model
* Added StopLossPrice and TakeProfitPrice to SharedPosition response model
* Added TriggerPrice and IsTriggerOrder to SharedSpotOrder response model
* Added QuoteVolume property to SharedSpotTicker response model
* Added AssetAlias configuration models
* Added static ExchangeSymbolCache for tracking symbol information from exchanges
* Added static CallResult.SuccessResult to be used instead of constructing success CallResult instance
* Added static ApplyRules, RandomHexString and RandomLong helper methods to ExchangeHelpers class
* Added AsErrorWithData To CallResult
* Added OriginalData property to CallResult
* Added support for adjusting the rate limit key per call, allowing for ratelimiting depending on request parameters
* Added implementation for integration testing ISymbolOrderBook instances
* Added implementation for integration testing socket subscriptions
* Added implementation for testing socket queries
* Updated request cancellation logging to Debug level
* Updated logging SourceContext to include the client type
* Updated some logging logic, errors no longer contain any data, exception are not logged as string but instead forwarded to structured logging
* Fixed warning for Enum parsing throwing exception and output warnings for each object in a response to only once to prevent slowing down execution
* Fixed memory leak in AsyncAutoRestEvent
* Fixed logging for ping frame timeout
* Fixed warning getting logged when user stops SymbolOrderBook instance
* Fixed socket client `UnsubscribeAll` not unsubscribing dedicated connections
* Fixed memory leak in Rest client cache
* Fixed integers bigger than int16 not getting correctly parsed to enums
* Fixed issue where the default options were overridden when using SetApiCredentials
* Removed Newtonsoft.Json dependency
* Removed legacy Rest client code
* Removed legacy ISpotClient and IFuturesClient support
2025-05-13 10:15:30 +02:00
Jkorf
3d6267da93 Added specific logging for user cancellation on rest requests instead of generic warning log 2025-03-05 09:02:25 +01:00
JKorf
8def7f32af Update UnsubscribeAll in socket client to also unsubscribe on a dedicated connection 2025-03-04 19:45:06 +01:00
Jkorf
ac295de9f6 Added referal link 2025-03-04 11:46:03 +01:00
Jkorf
d412e0895e Added DeepCoin reference and examples 2025-03-04 11:41:03 +01:00
Jkorf
1f9e2b4fcb Fixed websocket ping timeout not recognized for warning logging 2025-02-26 10:44:57 +01:00
James Carter
b13cff5a95
Fix memory leak in AsyncAutoResetEvent (#229)
* Fix memory leak in AsyncAutoResetEvent

CancellationTokenRegistration MUST be disposed, as the CancellationToken passed is saved for the lifetime of the Client, and registrations build up forever.
2025-02-24 08:33:26 +01:00
James Carter
4c050744ad
Allow specifying the ReceiveMessageBuffer size on Websockets (#228)
In order to support more User websocket connections, allow reducing the memory requirements for the receive buffer, keeping the default buffer.
2025-02-23 19:58:05 +01:00
JKorf
3b15c35a02 Added support for ratelimiting key suffix, allowing parameter based ratelimiting 2025-02-17 17:26:04 +01:00
Jkorf
cd78dbf575 Updated to version 8.8.0 2025-02-10 14:38:19 +01:00
Jkorf
a258532d6a Fixed DataTime copying in DataEvent 2025-02-10 14:32:25 +01:00
JKorf
d2a87a1069 Added additional enum values to default SupportIntervals for shared rest and socket kline operations 2025-02-09 21:43:18 +01:00
JKorf
e07f24ea0a Fixed various info-warnings and spelling issues 2025-02-09 21:25:26 +01:00
JKorf
024e8dcfe2 Added SharedKlineInterval values 2025-02-09 20:12:55 +01:00
JKorf
4bb5aae40a Split DataEvent.Timestamp in DataEvent.ReceivedTimestamp and 2025-02-09 16:40:28 +01:00
JKorf
dec94678ec Updated to version 8.7.4 2025-02-08 14:28:29 +01:00
JKorf
1a49fc8251 Fix exception when creating rest client for mono runtime 2025-02-08 14:25:19 +01:00
Jkorf
29b0875960 Updated examples 2025-02-07 13:50:00 +01:00
Jkorf
976ccab1da Added BitMEX reference 2025-02-07 13:20:54 +01:00
Jkorf
02bbd37bb6 Updated to version 8.7.3 2025-02-05 09:15:25 +01:00
Jkorf
1bbbec7f2b Fixed issue with serialization of nullable types in System.Text.Json ArrayConverter 2025-02-05 09:12:12 +01:00
Jkorf
0262f04913 Added handling of negative number DateTime deserialization to default 2025-02-05 08:25:59 +01:00
Jkorf
fd1ec17d72 Fix for unnecessary error message in logging when closing connection 2025-02-04 08:28:48 +01:00
Jkorf
4bdad7fe0c Updated SharedSymbol from class to record 2025-02-04 08:28:21 +01:00
Jkorf
74f73dc790 Updated to version 8.7.2 2025-01-27 13:24:08 +01:00
Jkorf
0527a8a76e Some small fixes in the System.Text.Json ArrayConverter, added support for flags in EnumConverter 2025-01-27 11:52:07 +01:00
Jkorf
c693eb8c02 Updated to version 8.7.1 2025-01-24 08:42:08 +01:00
Jkorf
3eb28c7fed Added HyperLiquid referral 2025-01-23 09:35:44 +01:00
JKorf
618c4922b9 Added Authenticated property to IBaseApiClient interface 2025-01-22 19:11:38 +01:00
Jkorf
c81b15861d Updated examples and docs with HyperLiquid references 2025-01-21 15:24:23 +01:00
Jkorf
4a5832cccd Updated to version 8.7.0 2025-01-21 14:02:42 +01:00
Jkorf
4e47c4cbdf Updated CheckForMissingInterfaces test 2025-01-21 14:00:30 +01:00
Jkorf
2af1520ecc Added PriceSignificationFigures to SharedSpotSymbol model 2025-01-21 14:00:08 +01:00
Jkorf
cf397af3ab Added GetMillisecondTimestampLong helper method to AuthenticationProvider 2025-01-21 13:47:05 +01:00
JKorf
a1479705e2 Fixed typo 2025-01-13 17:49:38 +01:00
Jkorf
175e23f110 Updated to version 8.6.1 2025-01-09 16:25:11 +01:00
Jkorf
9b7019ded2 Removed websocket Error callback when exception is expected 2025-01-09 16:23:34 +01:00
Jkorf
7904aa9ba7 Fixed websocket connection getting stuck after a ping frame timeout, removed unnecessary type restraints on RestApiClient.SendAsync methods 2025-01-09 16:18:13 +01:00
Jkorf
3fe6db589f Updated to version 8.6.0 2025-01-07 13:25:50 +01:00
Jkorf
625dccbbe4 Added ExchangeType enum, some small improvements 2025-01-07 13:22:16 +01:00
Jkorf
e650771d16 Added response headers parameter to RestApiClient.TryParseError method, added check for ServerRateLimitError on the result 2025-01-07 10:19:37 +01:00
Jkorf
3dad28b19d Added IFeeRestClient to service registration 2025-01-07 08:59:51 +01:00
Jkorf
2b9fda985e Add support for passing weight to apply to an individual ratelimit guard 2025-01-07 08:35:06 +01:00
JKorf
ff8759409b Use Convert.ToHexString if available 2025-01-06 21:38:42 +01:00
JKorf
0d9627c13f Changed socket no data reconnect message to LogLevel Warning 2024-12-23 20:03:56 +01:00
Jkorf
0179fd7e2a Fixed workflow automated tests 2024-12-23 14:43:23 +01:00
Jkorf
b8d0b0cf95 Workflow fix 2024-12-23 14:36:18 +01:00
Jkorf
73c42bd452 Updated to version 8.5.0 2024-12-23 14:25:39 +01:00
Jkorf
290be7f5e0 Added net9.0 build target, added KeepAliveTimeout for websocket connections 2024-12-23 14:14:47 +01:00
Jan Korf
0be1bb16e3
Feature/update settings (#225)
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
2024-12-23 08:49:58 +01:00
Jkorf
8605196390 Updated to version 8.4.5 2024-12-20 15:25:41 +01:00
Jkorf
460dd97537 Added EmptyArrayObjectConverter, added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor ctor 2024-12-20 15:21:21 +01:00
JKorf
1ec5984fad Updated to version 8.4.4 2024-12-08 10:16:19 +01:00
JKorf
8260c2661d Changed JsonConverterCtorAttribute to use type parameter instead of generic parameter to support .net framework 2024-12-08 10:12:39 +01:00
Jkorf
591c1dd405 docs 2024-12-04 10:44:35 +01:00
Jkorf
0164cdfcc4 docs 2024-12-04 09:15:23 +01:00
Jkorf
23a6cfff87 docs 2024-12-04 09:07:59 +01:00
Jkorf
fdcdb90a5f Added XT reference 2024-12-04 08:58:46 +01:00
Jkorf
0b7107401f Updated to version 8.4.3 2024-12-03 09:59:36 +01:00
JKorf
06add65354 Fixed KlineTracker update handling 2024-12-02 21:46:30 +01:00
Jkorf
773d288497 Updated to version 8.4.2 2024-12-02 14:39:27 +01:00
Jkorf
fd4e8da938 Removed special characters in ClientOrderIdSeperator to adhere to field content rules 2024-12-02 14:38:22 +01:00
Jkorf
271743b669 Updated to version 8.4.1 2024-12-02 13:15:31 +01:00
Jkorf
f4797caf37 Added replace converter, added library helpers class 2024-12-02 13:13:56 +01:00
Jkorf
62c9769c72 Updated to version 8.4.0 2024-11-28 14:24:00 +01:00
Jkorf
92d7bc1e2e Added GetFeesAsync Shared REST client support, Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions 2024-11-28 14:18:09 +01:00
Jkorf
99e4f96f63 Updated some testing code 2024-11-27 13:07:26 +01:00
Jkorf
94d8afe149 Updated package dependency versions 2024-11-27 13:07:05 +01:00
Jkorf
90ad59c63a Added comma split enum string json converter 2024-11-22 16:20:09 +01:00
Jkorf
c2273edfaa Added library options class 2024-11-20 09:52:38 +01:00
Jkorf
236283f4dd Update example-config.json 2024-11-19 14:53:58 +01:00
Jkorf
b66f12ff75 Updated to version 8.3.0 2024-11-19 11:52:46 +01:00
Jkorf
0403384beb Fixed warnings 2024-11-19 11:50:55 +01:00
Jan Korf
7d7bc35869
Client Configuration (#219)
Added support for IOptions injection, allowing options to be read from IConfiguration
Small refactor on client options internals
Updated HttpClient to be static field to be
2024-11-19 11:44:30 +01:00
Jkorf
48797038be Added rate limit update event 2024-11-13 14:29:43 +01:00
Jkorf
d21792d04c Added handling of Infinity values in decimal converter 2024-11-13 11:39:55 +01:00
Jkorf
8414e9d94f Fixed concurrency issue when unsubscribing websocket subscription during reconnection 2024-11-12 16:21:15 +01:00
Jkorf
ab0243445d Updated docs and examples, added WhiteBit reference 2024-11-07 11:39:44 +01:00
Jkorf
f2cf70b02f Updated to version 8.2.0 2024-11-06 14:00:23 +01:00
Jkorf
9ff417bba8 Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token 2024-11-06 13:56:33 +01:00
Jkorf
6b43d08a4d Added support for not allowing duplicate subscription topics on the same websocket connection 2024-11-06 11:39:11 +01:00
Jkorf
39bf7fe9b9 Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T> 2024-11-06 11:20:37 +01:00
Jkorf
b5893c3b60 Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable 2024-11-06 11:20:11 +01:00
Jkorf
15657ba683 Updated to version 8.1.1 2024-11-01 10:38:30 +01:00
Jkorf
1aed9f0c67 Fixed System.Text.Json ArrayConverter not passing serializer options to nested deserialization, fixed creating new serializer options each time a JsonConverter attribute is encountered 2024-11-01 10:34:07 +01:00
Jkorf
17f1560310 Fixed socket connections trying to authenticated connection when it's marked as dedicated request connection even when no authentication is needed 2024-11-01 09:38:01 +01:00
Jkorf
41de0a3150 Update index.html 2024-10-28 16:14:54 +01:00
Jkorf
3e410be611 Update index.html 2024-10-28 16:11:01 +01:00
Jkorf
be75449e4a Updated examples, added trackers example 2024-10-28 15:38:25 +01:00
Jkorf
b1b05c8f6b Added catch around HttpClientHandler.AutomaticDecompression setting as it's not support on Blazor WASM 2024-10-28 13:41:58 +01:00
Jkorf
a0e588c3de Updated to version 8.1.0 2024-10-28 10:44:45 +01:00
Jan Korf
9e86a08327
Trackers (#218)
Fix for intermittently failing rate limiting test
Added ConnectionId to RequestDefinition to correctly handle connection and path rate limiting configuration
Added ValidateMessage method to websocket Query object to filter messages even though it is matched to the query based on the  ListenIdentifier
Added KlineTracker and TradeTracker implementation
2024-10-28 10:36:19 +01:00
Jkorf
ed007b5272 Added overload for Create method in OrderBookFactory using SharedSymbol 2024-10-23 14:04:10 +02:00
Jkorf
bdd5526244 Added Side to SharedTrade model 2024-10-23 14:01:17 +02:00
Jkorf
ce35e30688 Updated documentation and examples 2024-10-22 16:20:27 +02:00
Jkorf
b40f72b1b0 Added Crypto.com reference 2024-10-22 15:37:56 +02:00
Jkorf
31a6cf285b Doc fix 2024-10-22 11:56:59 +02:00
Jkorf
1842f4fda0 Set ApiCredentials in the client specific options to prevent unknown client credentials when using SetApiCredentials method on client 2024-10-22 10:15:17 +02:00
Jkorf
7a58902ab6 Made SharedFuturesTicker last/high/low price properties nullable 2024-10-22 09:54:44 +02:00
Jkorf
3cb91296ca Added DoHandleReset method for websocket subscriptions 2024-10-22 09:47:14 +02:00
Jkorf
130ed40580 Comment 2024-10-21 16:29:56 +02:00
Jkorf
94cb2caf0b Added System.Text.Json ArrayConverter Write implementation 2024-10-15 10:51:19 +02:00
Jkorf
917d060827 Updated to version 8.0.3 2024-10-14 14:13:38 +02:00
JKorf
c58bc2be07 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-10-12 13:06:28 +02:00
JKorf
ff3356e2b4 Added Authenticated property on base client and shared client 2024-10-12 13:06:22 +02:00
Jkorf
79434c7be5 Implemented GetValues System.Text.Json in message accessor 2024-10-11 16:02:17 +02:00
Jkorf
168dabc11f Added fallback for unparsable value in System.Text.Json NumberStringConverter 2024-10-09 15:42:11 +02:00
Jkorf
71ee263683 Added support for duplicate array indexes in System.Text.Json ArrayConverter 2024-10-09 15:41:43 +02:00
Jkorf
7239b9c289 docs 2024-10-09 10:25:57 +02:00
Jkorf
84d36544e4 Updated to version 8.0.2 2024-10-09 08:49:19 +02:00
Jkorf
a71f57ae7f Updated dependency versions, including System.Text.Json containing a vulnerability fix 2024-10-09 08:46:15 +02:00
Jkorf
6e5bcd5e9a Some small doc fixes 2024-10-07 15:30:09 +02:00
Jkorf
4131e563c3 Added Coinbase reference, updated examples 2024-10-07 15:20:12 +02:00
Jkorf
613766dbca Updated to version 8.0.1 2024-10-07 13:03:08 +02:00
Jkorf
23b07d709e Added testing check for year 1 datetime 2024-10-07 12:57:23 +02:00
Jkorf
bbbdac2fd3 Added ToRfc3339String extension method for DateTime 2024-10-07 12:57:04 +02:00
Jkorf
c614b7869c Added check for datetime year 1 to be deserialized as null 2024-10-04 18:50:27 +02:00
Jkorf
1f31e4a9d7 Added cached lib versions properties 2024-10-04 18:49:58 +02:00
Jkorf
6cb6cd6b11 Note 2024-10-03 16:33:00 +02:00
Jkorf
17ffec329f Fixed typo 2024-09-27 15:15:09 +02:00
Jkorf
7a3927ef49 Add parameters documentation for shared clients 2024-09-27 15:13:50 +02:00
Jkorf
c1b0437c93 Updated examples 2024-09-27 13:53:33 +02:00
Jkorf
23e947f258 Updated to version 8.0.0 2024-09-27 10:55:29 +02:00
Jan Korf
b8686d60b9
Shared exchange functionality (#214) 2024-09-27 09:17:44 +02:00
Jonnern
5d3de52da6
feature: Handle error 429 when connecting websocket (#213)
* feature: Handle error 429 when connecting websocket

* Add preprocessor directive for NET6_0_OR_GREATER when checking for connection rate limit
2024-09-24 12:50:59 +02:00
Jonnern
fee18fd183
Dispose ClientWebSocket before creating a new (#212) 2024-09-17 11:59:58 +02:00
JKorf
3a43d461a3 Fixed workflow dotnet version 2024-08-29 16:45:34 +02:00
Jkorf
5f409efad3 Updated dotnet version unit tests 2024-08-29 14:01:59 +02:00
JKorf
cc1f0796fe Updated to version 7.11.2 2024-08-28 19:17:06 +02:00
JKorf
b1cd9b5412 Fixed exception being thrown when waiting was canceled during rate limiting 2024-08-28 19:10:08 +02:00
Jonnern
42003a0247
Fix issue where SemaphoreSlim is released twice in RateLimitGate (#210) 2024-08-28 12:25:09 +02:00
JKorf
d89c2bde94 Updated to version 7.11.1 2024-08-25 18:41:33 +02:00
JKorf
3e6bdaafc6 Improved closing logic websockets 2024-08-25 18:38:37 +02:00
JKorf
93e4722a81 Added testing checks for JsonInclude attribute for internal properties 2024-08-08 09:20:26 +02:00
JKorf
355ecb03da Updated to version 7.11.0 2024-08-07 18:42:00 +02:00
JKorf
994c527c1d Fixed warning 2024-08-07 18:39:01 +02:00
JKorf
69b2e2045e Fixed System.Text.Json tests not correctly checking capitalization 2024-08-07 16:49:12 +02:00
JKorf
7fde8bf5da Added converters/handling for values too big to fit decimal 2024-08-07 14:00:50 +02:00
JKorf
637070a7ae Fixed some warnings, added support for number deserialization when requesting string in STJ MessageAccessor.GetValue<T> 2024-08-06 15:37:15 +02:00
JKorf
7be75f72a7 Add check for null string to decimal converter 2024-08-05 21:45:01 +02:00
JKorf
e3fece41f3 Small test fixes 2024-08-05 16:13:08 +02:00
JKorf
87b0c8d7a2 Docs 2024-08-02 13:28:42 +02:00
JKorf
ca9a711f22 Support too large numbers for long value in NumberStringConverter, fall back to string 2024-08-02 12:07:15 +02:00
JKorf
27597bc994 Fix test 2024-08-02 08:59:14 +02:00
JKorf
776d75170d Fixed websocket client trying to unsubscribe subscription when the connection will be closed anyway 2024-08-02 08:59:04 +02:00
JKorf
949780a9ad Removed SecureString usage throughout the library, removed some object allocations, removed some unused extension methods 2024-08-01 22:43:06 +02:00
JKorf
2f64cd9f05 Add BitMart reference 2024-07-30 22:14:25 +02:00
JKorf
185dfeb6fb Added ParseString to EnumConverter for manual calling 2024-07-28 22:39:38 +02:00
JKorf
68067d6258 Updated to version 7.10.0 2024-07-26 14:29:07 +02:00
JKorf
b309deb0c4 Small fixed/improvements. Added deflate stream method 2024-07-25 18:47:33 +02:00
JKorf
e1dafdf0dd Added AddSecondsString and AddOptionalSecondsString method to ParameterCollection 2024-07-24 10:22:35 +02:00
JKorf
fd7b5f0f0f Added RestIntergrationTest base class for running integration tests 2024-07-23 16:58:10 +02:00
JKorf
3b735d66fd Improved socket reconnect logic so unsubscribing while processing resubscribing won't result in a ghost subscription 2024-07-23 12:01:31 +02:00
JKorf
3cd505ac8b Fixed order book logging 2024-07-23 12:00:15 +02:00
JKorf
81d856d78d Fixed System.Text.Json array converter deserialization when skipping indexes, added NumberStringConverter 2024-07-23 12:00:08 +02:00
JKorf
11c1ad871a Docs 2024-07-19 10:15:21 +02:00
JKorf
ffcb7db8ff Updated to version 7.9.0 2024-07-16 19:01:03 +02:00
JKorf
02432e5109 Added small performance improvements in SystemTextJsonMessageAccessor 2024-07-16 15:54:11 +02:00
JKorf
a85bfb4432 Added support for requiring multiple responses for queries, fixed possible exception when closing connection, added ToString override DataEvent object 2024-07-16 15:23:09 +02:00
JKorf
17d85fdd85 Added WebCallResult As<T> methods for untyped WebCallResult 2024-07-16 08:35:19 +02:00
JKorf
5e0733d7f4 Fixed trying to authenticate dedicated connection when no credentials are set 2024-07-14 20:59:22 +02:00
JKorf
28d5287bd4 Changed logging extensions to public so overridden methods can access them 2024-07-11 19:19:35 +02:00
JKorf
ef5097589a Test compare improvements 2024-07-10 16:57:30 +02:00
JKorf
f287ec1fa4 Added some checks socket connections 2024-07-10 14:12:07 +02:00
JKorf
28da93af9d Test fixes 2024-07-10 10:18:58 +02:00
JKorf
0d5bdf5095 Bumped System.Text.Json package to 8.0.4 2024-07-10 10:09:36 +02:00
JKorf
8dac3d7aa6 Refactor to prevent usage of thread block ManualResetEvent in socket message handling 2024-07-09 16:50:08 +02:00
JKorf
6951f31be7 Fix ArrayConverter error for nullable types 2024-07-05 16:34:58 +02:00
JKorf
630f85ec49 Updated to version 7.8.0 2024-07-02 20:17:01 +02:00
JKorf
9ec4f2276f Updated single endpoint limit configuration, added LongConverter, updated SystemTextJsonComparer logic 2024-07-02 16:13:10 +02:00
JKorf
0a0c66541e Updated to version 7.7.3 2024-06-26 19:11:13 +02:00
JKorf
bb4199620e Added caching docs 2024-06-26 15:33:31 +02:00
JKorf
8a83cd2cb8 Array comparison updates for unit tests 2024-06-26 15:13:21 +02:00
JKorf
fcfeaf568f Fixed request ids not matching 2024-06-26 11:21:52 +02:00
JKorf
25567ea434 Added nullable int converter for System.Text.Json 2024-06-25 20:52:35 +02:00
JKorf
1ab85d4c26 Updated to version 7.7.2 2024-06-25 16:42:45 +02:00
JKorf
be68115099 Fix for ratelimiting possibly creating negative waits 2024-06-25 16:14:09 +02:00
JKorf
ff0550b0fb Updated to version 7.7.1 2024-06-23 22:40:13 +02:00
JKorf
1ab1e008fc Some fixes for caching 2024-06-23 22:39:54 +02:00
JKorf
6f30c72608 Updated to version 7.7.0 2024-06-23 15:38:40 +02:00
JKorf
e927bc3d20 Close socket when connecting and authentication fails 2024-06-23 14:55:20 +02:00
JKorf
09ed7d1436 Small improvements 2024-06-23 14:41:59 +02:00
JKorf
6fed657ea6 Added ObjectStringConverter for double serialized objects 2024-06-21 16:49:02 +02:00
Jonnern
1555f8da0c
Fix issue with rate limit guard waiting sub ms (#204) 2024-06-21 16:47:14 +02:00
JKorf
68b28fc875 Small improvements 2024-06-19 17:01:26 +02:00
JKorf
5d50d8cde8 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-06-17 17:29:57 +02:00
JKorf
9ff673d8be Fixed unhandled exception when websocket connection is interupted while waiting for ratelimit 2024-06-17 10:53:22 +02:00
JKorf
3e5a34fb56 Added dedicated request websocket connection support 2024-06-16 16:55:44 +02:00
JKorf
64ee50d98c Added SocketConnection as parameter to GetAuthenticationRequest socket api client 2024-06-14 14:06:57 +02:00
JKorf
6a105c6f8f Added support for caching GET requests 2024-06-13 16:29:02 +02:00
JKorf
287aadc720 Added CancellationToken support for websocket queries 2024-06-13 11:58:52 +02:00
JKorf
7229438a0b Docs 2024-06-12 21:42:20 +02:00
JKorf
444af98a15 Added GateIo reference 2024-06-12 19:23:34 +02:00
JKorf
70c6fa1bbb Fixed tests 2024-06-12 18:05:26 +02:00
JKorf
d27f394b46 Removed HandleUpdatesBeforeConfirmation flag, allow messages to trigger listeners even if not confirmed and mark as confirmed then. Updated websocket reconnection delay handling 2024-06-12 16:56:06 +02:00
JKorf
c8c98e13d0 Updated to version 7.6.0 2024-06-11 16:32:28 +02:00
Jan Korf
9fcd722991
Feature/body uri param split (#203)
* Added support for specifying seperate uri and body parameters
* Added support for different message and handling generic types on socket queries
* Split DataEvent.Topic into StreamId and Symbol properties
* Added support for negative time values parsing
* Added some helper methods for converting DataEvent to CallResult
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
* Updated some testing methods
2024-06-11 16:23:48 +02:00
JKorf
8080ecccc0 Added support for Patch requests, added SetBody to ParameterCollection for directly setting the request body 2024-06-04 09:54:24 +02:00
JKorf
4b6fa9a1b1 Delete release.yml 2024-06-02 09:38:23 +02:00
JKorf
0b6dbde7d4 docs 2024-06-01 22:56:43 +02:00
JKorf
fe4d63ba75 Update index.html 2024-06-01 22:17:26 +02:00
JKorf
04bd3727ca Updated version 2024-05-07 09:56:59 +02:00
JKorf
7e6fcd03c2 Update release.yml 2024-05-07 09:50:22 +02:00
JKorf
fde8d6353b Fixed SetApiCredentials not correctly being used by rate limiter causing exception 2024-05-07 09:43:37 +02:00
JKorf
41b996168a Create release.yml 2024-05-05 14:17:13 +02:00
JKorf
b26f8fb900 Updated version 2024-05-03 16:05:44 +02:00
JKorf
bdbbc61d86 More supported testing comparing 2024-05-03 16:03:49 +02:00
JKorf
d64e200f2f Test implementation fixes 2024-05-02 22:29:32 +02:00
JKorf
71d54e2f9a Updated version 2024-05-01 19:54:07 +02:00
JKorf
ba3975993f Added multilayer nested compare support for testing 2024-05-01 19:49:14 +02:00
Jan Korf
050286ecd1
Unit testing update (#199) 2024-05-01 19:24:53 +02:00
JKorf
96c9a55c48 Update index.html 2024-04-29 15:33:09 +02:00
JKorf
a20cbb2f1c Update index.html 2024-04-29 14:57:20 +02:00
JKorf
2e957d7d9e Docs 2024-04-29 14:40:34 +02:00
JKorf
18d0341056 Update README.md 2024-04-28 14:10:00 +02:00
JKorf
a2bfed2433 Update README.md 2024-04-28 14:09:37 +02:00
JKorf
67299338a8 Updated version 2024-04-28 10:59:59 +02:00
Jan Korf
971c049c5f
Feature/cryptoclients update (#198) 2024-04-28 10:56:51 +02:00
JKorf
bb7ba5ea49 Updated version 2024-04-23 19:03:34 +02:00
JKorf
747c986644 Update DateTimeConverter.cs 2024-04-23 16:44:42 +02:00
JKorf
d88087c8ac Fix concurrency issue request definition cache 2024-04-23 14:56:10 +02:00
JKorf
968bdc330e Added new datetime format support 2024-04-23 14:55:57 +02:00
JKorf
7b49562c1d Fixed reconnect url logging even when url hasn't changed 2024-04-23 09:11:05 +02:00
JKorf
24ba60da47 Added error message context 2024-04-21 11:48:11 +02:00
JKorf
ed5a07fbdb Updated version 2024-04-19 11:47:21 +02:00
JKorf
3d3a9b88e7 Fix for endpoint specific rate limiting throwing exception 2024-04-19 11:45:46 +02:00
JKorf
b2f9d5753e Updated version 2024-04-18 19:25:53 +02:00
JKorf
de46c7bd1d Don't mark system subscriptions as unconfirmed when reconnecting 2024-04-18 13:39:00 +02:00
JKorf
5b11d94f73 Docs 2024-04-17 13:57:00 +02:00
JKorf
6f915a3739 Updated version 2024-04-17 13:39:52 +02:00
JKorf
d5c4b1bd01 Rename RatelimiterEnabled option to RateLimiterEnabled 2024-04-17 13:37:45 +02:00
Jan Korf
1b1961db00
Feature/ratelimit refactor (#197) 2024-04-16 14:55:27 +02:00
Jonnern
2dbd5be924
Get the state of ApiClient, SocketConnection, and Subscription as a record (#195) 2024-04-16 14:37:00 +02:00
JKorf
24c40d2dc6 Updated version 2024-04-05 20:09:37 +02:00
JKorf
5ef6feb996 Added readme for docs folder 2024-04-03 16:52:41 +02:00
JKorf
85dad6f6f0 Fix BoolConverter writing 2024-04-03 09:49:34 +02:00
JKorf
3cdcf0d9be Improved SystemTextJson message accessor value retrieval 2024-03-29 15:55:19 +01:00
JKorf
b90a0a71e9 Socket connection improvements on reconnect 2024-03-28 20:35:49 +01:00
JKorf
e62786a70f Update index.html 2024-03-27 16:17:37 +01:00
JKorf
87722f2d28 BingX example docs 2024-03-26 09:33:14 +01:00
JKorf
a86276f18d Added BingX to docs 2024-03-25 22:09:28 +01:00
JKorf
f432a66016 Updated examples 2024-03-25 19:56:21 +01:00
JKorf
9e2910d2ec Update README.md 2024-03-24 21:15:11 +01:00
JKorf
46fbc1eb85 Updated version 2024-03-24 11:49:29 +01:00
JKorf
f397d3ab94 Small logging changes 2024-03-24 10:28:55 +01:00
JKorf
81a2da1f3f Fix test 2024-03-22 17:50:12 +01:00
JKorf
af3303c7b8 Order book logging 2024-03-22 17:02:21 +01:00
JKorf
8ddd9ecf22 Some small improvements and fixes 2024-03-22 16:42:01 +01:00
Jonnern
de72fe4fb9
Implement high-performance logging (#193)
* Implement high-performance logging
2024-03-22 16:39:32 +01:00
JKorf
108c8fc183 Small fixes 2024-03-21 21:34:15 +01:00
JKorf
e86713e949 Performance improvements 2024-03-21 16:46:17 +01:00
JKorf
db9fba4cf2 Added specific type fields for NodeAccessor to prevent boxing 2024-03-21 14:09:49 +01:00
JKorf
926802d953 Fix for HandleUpdatesBeforeConfirmation if set on subscription itself 2024-03-20 12:03:19 +01:00
JKorf
87f5e12b60 Fixed original data not available in socket parsing 2024-03-18 15:29:42 +01:00
JKorf
034eb83bae Updated version 2024-03-16 15:25:07 +01:00
JKorf
7f29275851 Added Exchange property to ISocketClient interface 2024-03-16 14:47:12 +01:00
Jan Korf
2fb3442800
Feature/system.text.json (#192)
Initial support for System.Text.Json and some refactoring
2024-03-16 14:45:36 +01:00
Jonnern
462c857bba
Add tests for subscription confirmation (#191)
* Add tests for subscription confirmation
2024-03-08 08:34:14 +01:00
JKorf
61aa589cda Added checks 2024-03-04 16:45:51 +01:00
JKorf
27704bf090 Docs 2024-03-01 14:29:26 +01:00
JKorf
4c899861b1 Docs 2024-03-01 14:25:38 +01:00
JKorf
0cff678c2d Converter fixes 2024-02-26 14:28:34 +01:00
JKorf
d18514d73c Updated package versions examples 2024-02-25 16:51:58 +01:00
JKorf
84736cac3f Fixed docs reference 2024-02-25 16:25:20 +01:00
JKorf
cda1cce495 Updated docs 2024-02-25 14:54:33 +01:00
JKorf
fe3a0afd6c Docs 2024-02-24 19:54:27 +01:00
Jan Korf
d533557324
Websocket refactoring (#190)
Websocket refactoring
2024-02-24 19:21:47 +01:00
JKorf
d91755dff5 Updated version 2024-01-09 18:37:11 +01:00
JKorf
27d49a6093 Added support null and empty string values to BoolConverter 2024-01-09 18:36:20 +01:00
JKorf
ad1bdd9a3f Fixed error 2024-01-04 21:33:10 +01:00
JKorf
4f2d7abc7e Updated version 2024-01-04 21:32:27 +01:00
JKorf
ef9de5e338 Fixed parsing of string datetime value "0.00000" 2024-01-04 21:31:51 +01:00
JKorf
8b513e51b9 Updated version 2023-12-02 15:17:34 +01:00
JKorf
d43b38a23a Fix requestBodyFormat parameter usage 2023-12-02 15:15:38 +01:00
JKorf
0987c0f9d1 Updated version 2023-12-02 14:22:12 +01:00
JKorf
e2dde77023 Added DecimalStringWriter json converter, added support for specifying body content type on a per request basis 2023-12-02 14:20:41 +01:00
JKorf
104ac7caad Updated response logging, added RequestId to WebCallResult 2023-11-30 14:19:58 +01:00
JKorf
8788dd3deb Updated version 2023-10-28 15:19:11 +02:00
JKorf
f64cc5e9cf Added additional helpers methods 2023-10-28 15:17:10 +02:00
JKorf
75d1bbc6e8 Updated examples package versions 2023-10-24 21:11:50 +02:00
JKorf
b621aa7e65 Updated version 2023-10-24 18:44:22 +02:00
JKorf
9783108695 Added support for writing ints to EnumConverter 2023-10-24 18:41:38 +02:00
JKorf
6ba32fe280 Removed some things for internal use from interfaces 2023-10-12 22:25:25 +02:00
JKorf
f75cc75bbc Added SerializerOptions helper class for setting default serializer, Added ParameterCollection for easier parameter definition, Added extra encryption helper methods on AuthenticationProvider 2023-10-12 22:03:09 +02:00
JKorf
2109b65a8e Fixed incorrect options docs 2023-10-09 21:36:12 +02:00
JKorf
a472751638 Updated Examples 2023-10-09 20:43:08 +02:00
JKorf
f08ed16f2a Updated version 2023-10-08 17:01:35 +02:00
JKorf
212d457a6a Added UpdateType to DataEvent model, added additional scenarios to BoolConverter, updated some logging 2023-10-08 16:59:43 +02:00
JKorf
ac5f333766 Updated version 2023-09-23 21:16:09 +02:00
JKorf
640e4387c1 Added BoolConverter, added parameter for showing warning message to EnumConverter 2023-09-23 21:13:49 +02:00
JKorf
a16b19019f Updated version 2023-09-18 20:13:00 +02:00
JKorf
2443f576ac Fix for concurrency exception 2023-09-18 20:02:01 +02:00
JKorf
4fd7e44015 Logging 2023-09-16 18:26:10 +02:00
JKorf
a0a3bda1c5 Updated version 2023-09-11 20:59:44 +02:00
JKorf
6bda7a3c73 Fixed nullreference if no Retry-After is returned after with a ratelimit error 2023-09-11 20:57:46 +02:00
JKorf
69a7a714cd Updated rate limiters to support multiple instances 2023-09-11 20:16:50 +02:00
JKorf
48e2e6468e Updated version 2023-09-04 18:10:18 +02:00
JKorf
4017ac780f ArrayConverter update for handling exponent notation, EnumConverter fix for writing enum values 2023-09-04 18:09:01 +02:00
JKorf
a55cd1bb13 Docs 2023-08-26 20:04:38 +02:00
JKorf
b34129e148 Updated version 2023-08-24 21:25:37 +02:00
JKorf
be25a68c9c Ratelimiting for socket requests 2023-08-24 20:51:17 +02:00
JKorf
468cd5e48e Added RetryAfter property for ratelimit errors, added parsing of rate limit return 2023-08-21 21:34:26 +02:00
JKorf
262c4e4aa5 Dont process unsubscribe if there are no subscriptions 2023-08-21 20:11:31 +02:00
Jan Korf
4ccff6461f
Merge pull request #177 from ASolomatin/master
Ability for all Error derived classes to have Code and Data
2023-08-21 20:08:50 +02:00
JKorf
3bfa3ef389 Update index.md 2023-08-04 22:50:17 +02:00
JKorf
5238971bcc Update index.md 2023-08-04 22:44:09 +02:00
JKorf
2f5c904faf Added okx 2023-08-04 22:38:21 +02:00
JKorf
c62775813f Updated version 2023-07-23 13:52:07 +02:00
JKorf
f11b3754f0 Fix for proxy when not using DI 2023-07-23 10:01:13 +02:00
JKorf
3cbe0465e9 Docs 2023-07-11 21:38:48 +02:00
JKorf
5048aea722 Updated version 2023-07-05 21:58:35 +02:00
Aleksej Solomatin
8d35339ab2 Ability for all Error derived classes to have Code and Data
Proposal https://github.com/JKorf/CryptoExchange.Net/issues/176
2023-07-05 19:39:04 +03:00
Jkorf
c3316a51e7 Added properties dictionary to socket connection 2023-07-05 17:10:43 +02:00
JKorf
7ecf37064b Updated version 2023-06-29 20:10:35 +02:00
JKorf
18954f4f53 Added optional log level parameter for trace logger 2023-06-29 20:07:26 +02:00
JKorf
00bc245102 Updated examples 2023-06-26 20:45:28 +02:00
JKorf
273cab9fdb Docs 2023-06-25 21:43:30 +02:00
JKorf
84d0f0ec9e Updated version 2023-06-25 20:25:02 +02:00
JKorf
690f2a63e5 Squashed commit of the following:
commit 90f285d7f6bcd926ce9ca3d5832b1d70a5eae6ab
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jun 25 19:51:12 2023 +0200

    Docs

commit 72187035c703d1402b37bd2f4c3e066706f28d67
Author: JKorf <jankorf91@gmail.com>
Date:   Sat Jun 24 16:02:53 2023 +0200

    docs

commit 8411977292f1fb0b6e0705b1ad675b79a5311d90
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jun 23 18:25:15 2023 +0200

    wip

commit cb7d33aad5d2751104c8b8a6c6eadbf0d36b672c
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jun 2 19:26:26 2023 +0200

    wip

commit 4359a2d05ea1141cff516dab18f364a6ca854e18
Author: JKorf <jankorf91@gmail.com>
Date:   Wed May 31 20:51:36 2023 +0200

    wip

commit c6adb1b2f728d143f6bd667139c619581122a3c9
Author: JKorf <jankorf91@gmail.com>
Date:   Mon May 1 21:13:47 2023 +0200

    wip

commit 7fee733f82fa6ff574030452f0955c9e817647dd
Author: JKorf <jankorf91@gmail.com>
Date:   Thu Apr 27 13:02:56 2023 +0200

    wip

commit f8057313ffc9b0c31effcda71d35d105ea390971
Author: JKorf <jankorf91@gmail.com>
Date:   Mon Apr 17 21:37:51 2023 +0200

    wip
2023-06-25 19:58:46 +02:00
JKorf
19cc020852 Updated version 2023-04-14 21:03:36 +02:00
JKorf
1d4353e6d1 Fixed potential collection modified exception 2023-04-14 21:02:21 +02:00
JKorf
a02b3f88d7 Updated version 2023-04-01 19:00:22 +02:00
JKorf
af44ca4c9f Logging 2023-04-01 18:58:52 +02:00
JKorf
cf2b57bb96 Revert "Test some changes for robustness"
This reverts commit 1c33e297e71f6ba3e7eff98703e36f27c971855c.
2023-04-01 18:55:48 +02:00
JKorf
d0a2288910 Updated version 2023-03-18 14:58:09 +01:00
JKorf
89c11afc21 Fix Api key rate limit 2023-03-18 14:22:09 +01:00
JKorf
da6ed580f1 Added CalculateTradableAmount to SymbolOrderBook 2023-03-18 10:01:38 +01:00
JKorf
1c33e297e7 Test some changes for robustness 2023-02-18 10:41:26 +01:00
JKorf
0005534a95 Updated version 2023-02-14 20:10:13 +01:00
JKorf
11650f7c1a Updated some interfaces, made time syncing methods nullable for apis not using it, added optional retry checking, removed private key from api credentials, added better support for api credentials subclasses 2023-02-13 21:18:45 +01:00
JKorf
a222bb3f02 Fixed socket client options setting, added automatic unsubscribe if the subscription confirmation comes in after request timeout 2023-02-12 14:05:00 +01:00
JKorf
6361c5ef25 Added message on authentication exception 2023-02-08 21:55:56 +01:00
Jkorf
892e8a4508 Updated version 2022-12-08 11:59:02 +01:00
Jkorf
8336d373f3 Small fix for socket possibly reconnecting while it should close 2022-12-08 11:54:10 +01:00
Jkorf
71072680a8 Small fixes DelayAfterConnect option 2022-12-08 11:02:02 +01:00
Jan Korf
e13f105019
Merge pull request #167 from alokym86/master
order parameters before constructing request
2022-12-08 11:01:07 +01:00
alokym86
401577451e order parameters
order the request parameters before AuthenticateRequest invocation
2022-12-05 14:55:01 +02:00
JKorf
5c41ef1ee4 Updated version 2022-11-14 21:23:18 +01:00
JKorf
ad614830d1 Added optional delay after socket connection, added callback when reconnected socket to revitalize original request, fixed proxy setting socket 2022-11-13 19:47:33 +01:00
JKorf
3365837338 Updated tests 2022-11-13 15:31:28 +01:00
JKorf
66ac2972d6 wip 2022-11-02 17:59:33 +01:00
JKorf
0d3e05880a Wip client work 2022-10-31 21:41:30 +01:00
JKorf
997e71f3b7 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2022-10-28 20:02:37 +02:00
JKorf
b0fca4587d Added null check array converter 2022-10-28 20:02:25 +02:00
Jan Korf
c10671768d
Merge pull request #160 from kulikov-dev/fix/155-format-conversion-error
Fixing exponential format parsing
2022-10-15 13:34:00 +02:00
kulikov-dev
91e8123679 Fixing issue 155 with exponential format parsing (https://github.com/JKorf/CryptoExchange.Net/issues/155) 2022-10-15 09:57:14 +06:00
JKorf
417cf2f9ac Fixed nullability warning 2022-07-31 21:55:26 +02:00
JKorf
277be7ab9b Updated version 2022-07-31 21:53:39 +02:00
JKorf
45f3459f59 Made DataEvent ctor public 2022-07-31 21:47:22 +02:00
JKorf
98dad4a8ed Added handling for websocket options not being supported when running on WebAssembly 2022-07-31 21:45:19 +02:00
JKorf
1e5f19271b Updated support docs 2022-07-31 14:51:47 +02:00
JKorf
8abeeb4cf0 Update Clients.md 2022-07-29 18:57:04 +02:00
JKorf
cae0cd9ead Fixed EnumConverter serialization writing values without quotes 2022-07-27 21:26:17 +02:00
JKorf
811574ae01 Fixed websocket reconnecting too fast when reconnecting succeeds but resubscribing or authorization fails 2022-07-27 21:25:46 +02:00
JKorf
0ddecf7f8d Updated version 2022-07-19 19:13:26 +02:00
JKorf
5bcf50fb4d Fixed socket getting disconnected when no data timeout reached instead of being reconnected 2022-07-19 19:12:26 +02:00
JKorf
9f0654815d Updated version 2022-07-17 12:50:50 +02:00
JKorf
465e9f04f4 Added support for retrieving a reconnection url when socket connection is lost 2022-07-17 12:49:13 +02:00
JKorf
7c8cbfa4e2 Updated version 2022-07-16 21:06:30 +02:00
JKorf
4c79d13ff9 Set error to the response content when an error response is received which isn't json 2022-07-15 16:55:18 +02:00
JKorf
c815fad135 Fix for Message not handled when closing subscription, fix for reconnect loop 2022-07-12 22:06:38 +02:00
JKorf
41f17d0378 Don't close socket after failed auth when already closing 2022-07-11 18:56:51 +02:00
JKorf
50715ff2f7 Squashed commit of the following:
commit 0571ed17a0e502f689af6e8a5dbd0f05fd229496
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jul 10 19:56:27 2022 +0200

    Fixed tests

commit 99c331b389b58f09db3960adc7293d9b45d05caa
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jul 10 16:41:14 2022 +0200

    Updated version

commit 70f8bd203a00fbdef2b13526133a3b556cfc897f
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jul 10 16:36:00 2022 +0200

    Finished up websocket refactoring

commit 89b517c93684dc9c1e8a99bc600caaf6f9a4459e
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jul 8 20:24:58 2022 +0200

    wip

commit 91e33cc42c5725aece765b6c8f6a7f35ab87a80e
Author: JKorf <jankorf91@gmail.com>
Date:   Thu Jul 7 22:17:55 2022 +0200

    wip
2022-07-10 19:57:10 +02:00
JKorf
ea9375d582 Updated version 2022-06-12 15:36:04 +02:00
JKorf
2cf3c93e5e Cleanup 2022-06-12 15:35:35 +02:00
JKorf
ca888d8e41 Updated version 2022-06-12 15:31:03 +02:00
JKorf
2040b1c175 Fixed proxy setting not used on reconnecting socket 2022-06-12 15:26:11 +02:00
JKorf
d451c18821 No longer waiting for timesyncing to complete when it's not the first request 2022-06-12 15:21:22 +02:00
JKorf
c13dfa4461 Updated socket reconnection 2022-06-12 15:10:10 +02:00
JKorf
c2080ef75f Made MaxSocketConnections a setting, added support for changing log settings after creating client 2022-06-11 13:31:39 +02:00
Jan Korf
6b252e8024 Update TestSocket.cs 2022-05-24 22:36:55 +02:00
Jan Korf
d06bd5f176 Updated version 2022-05-24 18:56:37 +02:00
Jan Korf
d55fc8da65
Merge pull request #144 from tamaw/fix/missing-port-on-baseuri
Fixed: copying the port number when using a custom BaseAddress
2022-05-24 15:14:38 +02:00
Tama Waddell
01184f2c5d Added port to the other overloaded method 2022-05-24 21:13:08 +10:00
Jan Korf
cadc93c2f0
Merge pull request #143 from andriibratanin/bugfix/fix-nuget-discovery
Fix NuGet packages discovery for some IDEs
2022-05-24 10:21:21 +02:00
Tama Waddell
2600a51461 Included copying the port when using SetParameters 2022-05-24 15:16:06 +10:00
Andrii Bratanin
9e6a86ba8b Fix wrong case in csproj files of tests projects #142 2022-05-24 01:31:25 +03:00
Jan Korf
c4430d63fa Added KeepAliveInterval setting for socket connections 2022-05-23 22:05:04 +02:00
Jan Korf
f3e1cfef33 Updated version 2022-05-22 15:51:48 +02:00
Jan Korf
cc3053719c Make socket ConnectionLost run in a separate task to prevent issue with long running/exceptions in the handler 2022-05-22 15:46:30 +02:00
Jan Korf
cd6907e601 Merge branch 'master' of https://github.com/jkorf/CryptoExchange.Net 2022-05-22 14:35:07 +02:00
Jan Korf
8fe00693bd
Merge pull request #141 from nathan-datusarator/master
Add checks for Disposed
2022-05-22 11:41:55 +02:00
Jan Korf
fb90d1e015 Fixed exception when disposing client in reconnecting state 2022-05-22 11:35:39 +02:00
Jan Korf
4b44861e43 Added additional cases for no null/default handling in DateTimeConverter 2022-05-22 11:35:17 +02:00
Jan Korf
e42ca4ab5a Update FAQ.md 2022-05-21 10:17:59 +02:00
Nathan Pfluger
5b97f6dd67 Move Subscription Events into non-lambda so they can be removed on StopAsync 2022-05-12 10:00:44 -07:00
Nathan Pfluger
a9813ecb0a Add checks for Disposed 2022-05-12 09:05:27 -07:00
Jan Korf
c7069a4049 Updated version 2022-05-08 16:28:17 +02:00
Jan Korf
5683ae0b3c Small fix when closing socket 2022-05-08 16:25:45 +02:00
Jan Korf
1c8cf5ac98 Updated timestamp calculation to include latency 2022-05-08 15:23:47 +02:00
Jan Korf
ad7231ec56 Updated version 2022-05-01 13:59:56 +02:00
Jan Korf
7e4a607391 Fixed datetime converter considering dates over 2033 to be in the wrong format 2022-05-01 13:57:06 +02:00
Jan Korf
2d470d18e2 Added support for sending request with empty response 2022-05-01 13:50:23 +02:00
Jan Korf
cb9a766c3b Logging 2022-04-30 18:31:14 +02:00
Jan Korf
94b8184f7b Added handling for websocket send failing 2022-04-30 16:14:17 +02:00
Jan Korf
270ea06f24 Update SocketConnection.cs 2022-04-30 13:25:57 +02:00
Jan Korf
536afa92da wip 2022-04-24 15:25:50 +02:00
Jan Korf
11c48b3341 wip 2022-04-24 11:31:13 +02:00
Jan Korf
f514e172d7 wip 2022-04-24 09:29:08 +02:00
Jan Korf
1739769f87 Updated version 2022-04-14 15:40:51 +02:00
Jan Korf
7ccf643a34 Fixed tests 2022-04-14 15:09:38 +02:00
Jan Korf
edfaa650bf Moved some parameters from BaseRestClient to RestApiClient to support different setting between different sub api's 2022-04-14 15:06:52 +02:00
Jan Korf
13c81afb79
Merge pull request #135 from mohammadj22/ImproveWebSocketClientProxy
change SetProxy method in web socket client to support socks5 proxy.
2022-04-14 15:05:19 +02:00
Mohammad Reza
c4f4ddcdc5 Update SetProxy 2022-04-13 17:37:48 +04:30
Mohammad Reza
4f4d2ccff3 add Schema check 2022-04-13 13:18:38 +04:30
Mohammad Reza
4db43517b7 change SetProxy method to support socks5 proxy. 2022-04-07 12:44:12 +04:30
Jkorf
41f38e040e Added missing SetApiCredentials on socket client 2022-03-24 15:47:06 +01:00
Jkorf
3bdb50b1df Updated version 2022-03-10 10:29:48 +01:00
Jan Korf
0d1ca30ce3 Updated EnumConverter 2022-03-09 21:09:41 +01:00
Jkorf
d5697250e2 Updated version 2022-03-09 16:18:28 +01:00
Jkorf
a54a327f22 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2022-03-09 15:15:26 +01:00
Jan Korf
7166482a46
Added Stale Github workflow 2022-03-09 14:23:00 +01:00
Jkorf
839f509fef Removed ResubscribeMaxRetries default value of 5, Updated logging and log levels 2022-03-09 12:59:09 +01:00
Jkorf
949b205d4f Delete .travis.yml 2022-03-04 13:52:36 +01:00
Jkorf
e6c3251067 Updated build badge 2022-03-04 13:52:17 +01:00
Jkorf
77611a19c8 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2022-03-04 13:42:00 +01:00
Jkorf
9b950cab4c Updated dotnet versions examples/unit tests 2022-03-04 13:41:57 +01:00
Jan Korf
8b9172ba94
Added dotnet.yml for Github Actions build 2022-03-04 13:33:33 +01:00
Jkorf
d8a1d96e5c Updated version 2022-03-04 11:33:43 +01:00
Jkorf
7b370d47ce Added check invalid rate limit for request 2022-03-04 10:37:49 +01:00
Jkorf
434d9e3af6 Fixed array serialization support 2022-03-04 10:37:22 +01:00
Jkorf
42f95243d9 Updated version 2022-03-01 16:11:29 +01:00
Jkorf
e77ca7124e Fixed some issues in socket reconnection 2022-03-01 16:09:09 +01:00
Jkorf
5c51822996 Prevent potential duplicate reading of data on error 2022-03-01 10:43:11 +01:00
Jkorf
12fe94cbff Added ApiName to time sync state for logging 2022-03-01 10:41:51 +01:00
Jan Korf
4c4cfbb60e Updated version 2022-02-27 14:11:11 +01:00
Jan Korf
416f94484d Fixed rate limiting affecting time sync, added support for delegate parameters 2022-02-27 14:00:31 +01:00
Jkorf
52e79446f6 Updated version 2022-02-24 13:10:45 +01:00
Jkorf
63d4af8543 Updated version 2022-02-24 12:55:59 +01:00
Jan Korf
0c6e74911d Small changes for options 2022-02-23 22:04:43 +01:00
Jkorf
c792bc25b6 Small rework in options 2022-02-23 16:53:47 +01:00
Jkorf
e1f8b8b7b7 Update SymbolOrderBookTests.cs 2022-02-23 15:52:13 +01:00
Jkorf
7339cb9cc9 Fix for setting recalculation interval 2022-02-22 13:06:55 +01:00
Jkorf
5c99da6617 Dispose handling order book 2022-02-22 12:54:19 +01:00
Jkorf
c22b54c898 Squashed commit of the following:
commit 9450d447b9822470504e3031e57a65146c838e0e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Feb 18 11:05:46 2022 +0100

    Updated version

commit bc0b55f3372f32bf7dd6947f4ea4f02f1bfeaa05
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Feb 18 10:09:26 2022 +0100

    Added clientOrderId parameter to common clients

commit 31111006c728d4d1b513c32838ca5b92e33a4c4a
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Feb 17 16:32:53 2022 +0100

    Update SpotClient.razor

commit e7400ce334175961426daffd6827e08349e518b6
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Feb 17 16:10:49 2022 +0100

    Made some names more generic

commit 9bdef400daaed68d48f4be2c0a3311498bac5b1c
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Feb 15 11:38:41 2022 +0100

    Updated vesrion

commit 3b80a945eef9c42de8b19850b2e0fe45f2d6caa0
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Feb 15 11:34:50 2022 +0100

    docs

commit 0268e211e90956016652280c6d2b9b7ec4c4e701
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Feb 15 09:56:45 2022 +0100

    Immediate initial reconnect attempt when connection is lost

commit 6eb43c5218fcaab2e51538a93776d538f9b9e7fc
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Feb 11 13:59:05 2022 +0100

    Re-added recalculation interval

commit 1df63ab60c5e0f63f64d16a07ae452dd6bd92ee3
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 9 14:32:00 2022 +0100

    Updated version

commit 9461b57daa9ae4d74702b38de15cf2ee8c461263
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 9 13:37:12 2022 +0100

    Fix for time offset calculation not updating when offset is < 500ms

commit 105547d6b16d99258adb96c150bef7ffbc82b487
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 21:05:10 2022 +0100

    Updated version

commit 379ded6832d25ada47519f979c43c05daaf4d17c
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 20:29:57 2022 +0100

    Fixed tests

commit b18204a52d8c26650059fc88631ad9eccc505f15
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 20:28:08 2022 +0100

    Added CancellationToken support on Common client interface and SymbolOrderBook, improved SymbolOrderBook start/stop robustness

commit baa23c2eccb6f84c875be3c60e77c395e8b7cd90
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 14:56:32 2022 +0100

    Added GetSubscriptionByRequest method on socket connection

commit 7aad9482a540865c4f83bea7aaf763979e02cdba
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 2 10:57:06 2022 +0100

    Updated version

commit 6e4d9d225eb4076a3c2c6586c5a4cec391e04d00
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 2 09:42:22 2022 +0100

    Fixed exception when deserializing non-nullable datetime value '0' in .net framework

commit fd1a2bbda95314f4b03d1d0e0078171238429c7c
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 25 13:19:10 2022 +0100

    Updated version

commit 2ece04dd58f7524e4d50447fe1813d1c3ef7e5d4
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 25 13:17:25 2022 +0100

    Refactored use of AutoResetEvent to AsyncResetEvent in SymbolOrderBook

commit 893d0c723d55c026ab854d0945a03186828d59b3
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 25 13:01:21 2022 +0100

    Fixed DateTime converter for nanosecond times in string format

commit 2c43ee7554af43adee40082b4500bc1e9c041d36
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 24 15:56:24 2022 +0100

    Updated version version; fixed dependencies

commit 100a34d1a0372940dd6f02599c46306fed263c6c
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 24 14:37:15 2022 +0100

    Updated version

commit bb1071472f4170c2f250e8c3775e888b02b7a1ed
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 24 14:31:57 2022 +0100

    Re-added Common prefix for common enums to avoid conflicts with library namespaces

commit 37b1d18104851797e3bc617c976ad2962f183b90
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 21 15:25:33 2022 +0100

    Updated version

commit 325389cdf81e51ef829bb2c5edd45cd4adc00d09
Author: Jan Korf <jankorf91@gmail.com>
Date:   Thu Jan 20 21:08:51 2022 +0100

    Added FTX to console example

commit 3e23882572e42b54e30c0727f4002a8c3d620b88
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jan 20 16:21:42 2022 +0100

    Replaced Debug.WriteLine with Trace.WriteLine

commit 3cf5480cad23db99711486afbdb71e242f65de76
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Jan 19 22:07:22 2022 +0100

    Example

commit fe31cf156d76c41159ff4ffee02c720929a2aaa4
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Jan 19 16:35:08 2022 +0100

    Examples

commit 7427914cb76cc44dda2c095e90bf89a04cf802d0
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 16:46:43 2022 +0100

    Update index.md

commit 1bc62258140d4f11c3348ea6f32fc81ff67e0186
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 16:45:10 2022 +0100

    docs

commit 259fe6bfd12026161aa6bdb167e0a9da3e5eca6e
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:25:20 2022 +0100

    Update index.md

commit 5f9c075ac7fc8ae1d35b71ea8db99168c2945511
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:22:33 2022 +0100

    Update index.md

commit a26514016a0cebb86117be25987e425e47bfb2f9
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:13:35 2022 +0100

    Update index.md

commit 01a97412bffcbded0ade5406a0e482236fa6f3f4
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:12:02 2022 +0100

    docs

commit 24b503ca8cdeb7d1ab98467a38b6fc4905d53246
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 13:42:12 2022 +0100

    docs

commit 008b15b055bf6c4793f0ca26bca8a302f0b1614a
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 13:32:55 2022 +0100

    docs

commit 66fce6cb849ae86b0b71bdb625c2b4f905a0ba33
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jan 17 21:31:53 2022 +0100

    docs

commit 0f65701f902bfb290d4589d05debc2de4b6d5705
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jan 17 21:25:33 2022 +0100

    docs

commit f7a405a2e6e518ff1d9bef0a43ac0e0759aea1d2
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 16:32:50 2022 +0100

    docs

commit 55284c0549a38ae88cc7fe0c9f85fe8326bb1f3d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 15:51:03 2022 +0100

    docs

commit 5bfbcca25bf84ab429007555c9b87331d867172f
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 14:04:08 2022 +0100

    docs

commit cdbc0ba215cb978b0bc3e3d2fe23b874b3c07256
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:58:51 2022 +0100

    docs

commit e33e7c6775b9a355bdaf38a407bd3d4c09809ccb
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:47:58 2022 +0100

    docs

commit b65669659d189066d0ef6e19ff9c02fb27cd18f1
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:44:45 2022 +0100

    docs

commit e51b8632424965e25cee5f70386aed9b20601255
Merge: dbfe34f 088f35d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:36:51 2022 +0100

    Merge branch 'feature/new-cc' of https://github.com/JKorf/CryptoExchange.Net into feature/new-cc

commit dbfe34f53449c1d84948500d36fc0af7278befa9
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:35:46 2022 +0100

    Docs

commit 088f35d42099a60c8a820c603507b187f4038460
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:34:40 2022 +0100

    Set theme jekyll-theme-cayman

commit e77add4d1c84ccf1a7e8b55859883be36d72bdc3
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 15 15:26:38 2022 +0100

    Updated version

commit a37a2d6e31e3bbf13ed745761bc75c17638bddc2
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 15 15:23:52 2022 +0100

    Added CallResult tests, fixed response time not set

commit 8f6e853e13756260b78ff3b718ea5e6d200bab3e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 14 16:47:49 2022 +0100

    Added Request info and ResponseTime to WebCallResult, refactored CallResult ctors

commit c6bf0d67a45ae85f3b022b1257f4d9eeb322fa8e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 16:30:02 2022 +0100

    Fix typo

commit 996f3c2ced8caa8022f3e8b4e3c166b345a98842
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 16:23:42 2022 +0100

    Some options logging

commit fb9e9f9aa65b0fdd5866387316e9277f218482f1
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 15:10:27 2022 +0100

    Updated version

commit 52ebacaa212b1c663cdf7433876776f350692f3e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 15:05:51 2022 +0100

    Fixed symbol order book tostring not locking thread, Potential fix for request timeout showing unclear message

commit 6b4585993450daba95b84292fdc0d73f0f9dc7b7
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 14:08:35 2022 +0100

    Updated example

commit ebe332b724fbe4f7c9e2b2bf4864049e7dfa31d6
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 12:05:47 2022 +0100

    Updated version

commit 8c24b46fb32408afacea86c9504f4a463fae3c75
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 11:33:07 2022 +0100

    Fixed typo Comon -> Common

commit 7a195f662c6c339d7d69e88025803f497f1ae30d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 09:37:50 2022 +0100

    Updated example, removed global.json

commit 120132c45b9b8cc7703d0b9a9bbdba1f54e92f9b
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 1 20:30:35 2022 +0100

    Reverted conditional refs

commit b3b4ed3f3fd0fd0fa536e1fa245c05fdb65633e0
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 1 19:45:59 2022 +0100

    Updated version

commit f4b4c93e6473961875f114b47d3434714f57c8b9
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 1 19:40:50 2022 +0100

    Added new shared interface implementation

commit f8c3b37cdf3baa42715cf5b31e5884fd9f077db4
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue Dec 28 14:14:15 2021 +0100

    wip example

commit 0117737dfacb8f37f087ab8d59ae806ab66b1e55
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue Dec 28 14:13:14 2021 +0100

    Added conditional refs for Microsoft.Extensions, added DependencyInjection.Abstractions to support extension method on IServiceCollection

commit 02c1f874e17a9fae1579ed2ce5e8a6db99377738
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Dec 27 15:32:07 2021 +0100

    Updated version

commit b212842ec8048be584ae034c9cf2c28a97ecfa3e
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Dec 27 15:27:14 2021 +0100

    Added ExchangeName to IExchangeClient interface

commit c96e75d6c3ef0e28a492755b0c2bf2cc2531f4e1
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Dec 21 16:22:51 2021 +0100

    Updated version

commit c62fbda3d74c00c11e030f250a91494ab4b538d9
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Dec 17 14:17:30 2021 +0100

    Added ApiClients list for managing api credentials, requests made and dispose

commit 04b43257a549666d793674931640810c8f276c1e
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Dec 16 16:17:26 2021 +0100

    Update .gitignore

commit 8ba0ded16d12a7fadffe87d7819a7ddd9df457bc
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Dec 13 12:57:31 2021 +0100

    Fixed api credentials getting disposed, fixed DateTimeConverter losing precision

commit 5c665ad54ca40e473b236ddacf54aa9da563320e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Dec 10 16:35:42 2021 +0100

    Refactoring and comments

commit b7cd6a866acf4d91b48d4e50216f6627a299c3a7
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Dec 8 21:49:25 2021 +0100

    Auth work

commit c2105fe690c6b4a468d3d45e847a2e32172c702a
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Dec 8 16:20:44 2021 +0100

    Wip, support for time syncing, refactoring authentication

commit 8b479547ab7330a143502eccb51f0f38e88fe4b9
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Dec 7 15:47:55 2021 +0100

    Fixed release name

commit 2ab032b8718d6dcfbe0904eb5d8ad2cb6c4d2889
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Dec 7 15:47:14 2021 +0100

    Updated version

commit 48baaeb2d8579e4844aaeb3aac30017a71dcb460
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Dec 6 16:18:18 2021 +0100

    Added periodic identifier

commit 60ec18919a080f004bfa1f35dc604d372fc837d9
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sun Dec 5 17:26:55 2021 +0100

    Added quotes to log

commit 0818c6277b10f0b334de3145318ac1f6fb1596a8
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Dec 3 16:23:05 2021 +0100

    Small changes

commit 6d0120d564183984d77574921b401127934e43c7
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Dec 1 16:26:34 2021 +0100

    Comments, fix test

commit 3c3b5639f59e07fb5c70d120c15a247d32dfab98
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Dec 1 13:31:54 2021 +0100

    Refactor clients/options

commit 49de7e89ccf6e16dee3ffb10ca77f2f0e2720ac2
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Nov 30 10:31:45 2021 +0100

    Disposable changes, fixed tests

commit 69a6fabb790770b4302e8eb26f9e4acd62cc868d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Nov 29 16:43:27 2021 +0100

    Restruct

commit 9a266e44ced9d9f887fe9e664c1ca393ca008008
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Nov 26 09:32:26 2021 +0100

    Added enum converter

commit 78f81393a441ca34d067a20973e3410761cbf77a
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Nov 25 10:25:56 2021 +0100

    Removed old timestamp converters

commit 9ebe5de825ed81a4fe77563d602882f7e9847352
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Nov 24 19:32:37 2021 +0100

    Added AppendPath method

commit 8b619e82f2953c88e15c1a52e3a09b8de495dfed
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 24 16:39:14 2021 +0100

    Added DateTimeConverter as replacement for individual converters, fix for not closing socket when auth fails

commit 7ac7a11dfe87f1ad9b06eaf1327e334f255e0477
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 17 10:23:01 2021 +0100

    Resolved some code issues

commit 3784b0c62b2e0ddba3018fbf18340ee20c32f879
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Nov 15 16:36:30 2021 +0100

    Ratelimiter rework

commit cb1826da7acf730e32bbad43458662ca4e25f35a
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Nov 12 09:40:42 2021 +0100

    Documentation

commit f7445543f261d517bfafa4657eba0e4f7b013da7
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 10 16:44:46 2021 +0100

    Exposed order book id

commit 6c3462403f25382365ff8633f62bf8ca3194d343
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 10 13:18:52 2021 +0100

    Fixed tests

commit f83127590ac0b2fd0a9258c21458a05d714a1d14
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 3 08:27:03 2021 +0100

    wip

commit 23bbf0ef8869591e55d9e6822360d9edd9ef6c92
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Oct 27 12:57:23 2021 +0200

    Added cancellation token support for socket subscriptions

commit b7f1619aec8c09b93777bd6319c3adbc8216927b
Merge: 6ce6a46 f6af235
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Oct 26 15:39:52 2021 +0200

    Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net

commit 6ce6a46ca347468c23e21f561de41ec3fce51e3f
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Oct 26 15:39:50 2021 +0200

    Some renames
2022-02-18 11:06:34 +01:00
Jan Korf
8207a8f32a Update .gitignore 2021-12-15 22:09:11 +01:00
Jan Korf
27cd80d508 Update .gitignore 2021-12-15 21:35:42 +01:00
Jan Korf
f6af235f51 Added github sponsors to readme 2021-10-12 18:13:03 +02:00
998 changed files with 152706 additions and 11625 deletions

25
.github/workflows/dotnet.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: .NET
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal

31
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '33 20 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'No activity on this issue for 60 days. This issue will get closed if it receives no update within 14 more days.'
stale-pr-message: 'No activity on this PR for 60 days. This PR will get closed if it receives no update within 14 more days.'
close-issue-message: 'Closed for inactivity. Feel free to update this if the issue is still relevant.'
close-pr-message: 'Closed for inactivity. Feel free to update this if the PR is still relevant.'
exempt-issue-labels: 'Future'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
days-before-close: 14

View File

@ -1,7 +0,0 @@
language: csharp
mono: none
solution: CryptoExchange.Net.sln
dotnet: 5.0.103
script:
- dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.1"
- dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj

View File

@ -0,0 +1,530 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using ProtoBuf;
using ProtoBuf.Meta;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
#if NET5_0_OR_GREATER
public abstract class ProtobufMessageAccessor<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
TIntermediateType> : IMessageAccessor
#else
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
#endif
{
/// <summary>
/// The intermediate deserialization object
/// </summary>
protected TIntermediateType? _intermediateType;
/// <summary>
/// Runtime type model
/// </summary>
protected RuntimeTypeModel _model;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => _intermediateType;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageAccessor(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach (var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
if (value == null)
return null;
var valueType = value.GetType();
if (valueType.IsArray)
return NodeType.Array;
if (IsSimple(valueType))
return NodeType.Value;
return NodeType.Object;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach(var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
return (T?)value;
}
/// <inheritdoc />
public T?[]? GetValues<T>(MessagePath path)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null);
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class ProtobufStreamMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
var result = _model.Deserialize(type, _stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null)
{
try
{
var result = _model.Deserialize<T>(_stream);
return new CallResult<T>(result);
}
catch(Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
IsValid = true;
return Task.FromResult(CallResult.SuccessResult);
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_intermediateType = default;
}
}
/// <summary>
/// Protobuf byte message accessor
/// </summary>
public class ProtobufByteMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
using var stream = new MemoryStream(_bytes.ToArray());
stream.Position = 0;
var result = _model.Deserialize(type, stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public override CallResult<T> Deserialize<
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(MessagePath? path = null)
#else
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
#endif
{
try
{
var result = _model.Deserialize<T>(_bytes);
return new CallResult<T>(result);
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
_intermediateType = _model.Deserialize<TIntermediate>(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_intermediateType = default;
}
}
}

View File

@ -0,0 +1,53 @@
using CryptoExchange.Net.Interfaces;
using ProtoBuf.Meta;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <inheritdoc />
public class ProtobufMessageSerializer : IByteMessageSerializer
{
private RuntimeTypeModel _model;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageSerializer(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public byte[] Serialize<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(T message)
#else
public byte[] Serialize<T>(T message)
#endif
{
using var memoryStream = new MemoryStream();
_model.Serialize(memoryStream, message);
return memoryStream.ToArray();
}
}
}

View File

@ -0,0 +1,47 @@
<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.2" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,128 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>CryptoExchange.Net.Protobuf</name>
</assembly>
<members>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
<summary>
System.Text.Json message accessor
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
<summary>
The intermediate deserialization object
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
<summary>
Runtime type model
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
<summary>
System.Text.Json stream message accessor
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
<summary>
Protobuf byte message accessor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
<inheritdoc />
</member>
</members>
</doc>

View File

@ -0,0 +1,52 @@
# ![.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

View File

@ -1,9 +1,9 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
@ -24,8 +24,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
var result2 = await waiter2;
Assert.True(result1);
Assert.True(result2);
Assert.That(result1);
Assert.That(result2);
}
[Test]
@ -39,8 +39,8 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
var result2 = await waiter2;
Assert.True(result1);
Assert.True(result2);
Assert.That(result1);
Assert.That(result2);
}
[Test]
@ -55,14 +55,14 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.True(result1);
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
Assert.That(result1);
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
evnt.Set();
var result2 = await waiter2;
Assert.True(result2);
Assert.That(result2);
}
[Test]
@ -75,13 +75,13 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.True(result1);
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
Assert.That(result1);
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
evnt.Set();
var result2 = await waiter2;
Assert.True(result2);
Assert.That(result2);
}
[Test]
@ -105,12 +105,13 @@ namespace CryptoExchange.Net.UnitTests
for(var i = 1; i <= 10; i++)
{
evnt.Set();
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
}
await resultsWaiter;
Assert.AreEqual(10, results.Count(r => r));
Assert.That(10 == results.Count(r => r));
}
[Test]
@ -124,7 +125,7 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.True(result1);
Assert.That(result1);
}
[Test]
@ -136,7 +137,7 @@ namespace CryptoExchange.Net.UnitTests
var result1 = await waiter1;
Assert.False(result1);
ClassicAssert.False(result1);
}
}
}

View File

@ -1,88 +1,11 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class BaseClientTests
{
[TestCase(null, null)]
[TestCase("", "")]
[TestCase("test", null)]
[TestCase("test", "")]
[TestCase(null, "test")]
[TestCase("", "test")]
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
{
// arrange
// act
// assert
Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) }));
}
[TestCase]
public void SettingLogOutput_Should_RedirectLogOutput()
{
// arrange
var logger = new TestStringLogger();
var client = new TestBaseClient(new RestClientOptions("")
{
LogWriters = new List<ILogger> { logger }
});
// act
client.Log(LogLevel.Information, "Test");
// assert
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
}
[TestCase(LogLevel.None, LogLevel.Error, false)]
[TestCase(LogLevel.None, LogLevel.Warning, false)]
[TestCase(LogLevel.None, LogLevel.Information, false)]
[TestCase(LogLevel.None, LogLevel.Debug, false)]
[TestCase(LogLevel.Error, LogLevel.Error, true)]
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
[TestCase(LogLevel.Error, LogLevel.Information, false)]
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
[TestCase(LogLevel.Information, LogLevel.Error, true)]
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
[TestCase(LogLevel.Information, LogLevel.Information, true)]
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
[TestCase(null, LogLevel.Error, true)]
[TestCase(null, LogLevel.Warning, true)]
[TestCase(null, LogLevel.Information, true)]
[TestCase(null, LogLevel.Debug, true)]
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
{
// arrange
var logger = new TestStringLogger();
var client = new TestBaseClient(new RestClientOptions("")
{
LogWriters = new List<ILogger> { logger },
LogLevel = verbosity
});
// act
client.Log(testVerbosity, "Test");
// assert
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
}
[TestCase]
public void DeserializingValidJson_Should_GiveSuccessfulResult()
{
@ -90,10 +13,10 @@ namespace CryptoExchange.Net.UnitTests
var client = new TestBaseClient();
// act
var result = client.Deserialize<object>("{\"testProperty\": 123}");
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// assert
Assert.IsTrue(result.Success);
Assert.That(result.Success);
}
[TestCase]
@ -103,24 +26,24 @@ namespace CryptoExchange.Net.UnitTests
var client = new TestBaseClient();
// act
var result = client.Deserialize<object>("{\"testProperty\": 123");
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public void FillingPathParameters_Should_ResultInValidUrl()
[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")]
[TestCase("https://api.test.com/api/", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
[TestCase("https://api.test.com/", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
public void AppendPathTests(string baseUrl, string[] path, string expected)
{
// arrange
var client = new TestBaseClient();
// act
var result = client.FillParameters("http://test.api/{}/path/{}", "1", "test");
// assert
Assert.IsTrue(result == "http://test.api/1/path/test");
var result = baseUrl.AppendPath(path);
Assert.That(expected == result);
}
}
}

View File

@ -0,0 +1,184 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
internal class CallResultTests
{
[Test]
public void TestBasicErrorCallResult()
{
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
[Test]
public void TestBasicSuccessCallResult()
{
var result = new CallResult(null);
ClassicAssert.IsNull(result.Error);
Assert.That(result);
Assert.That(result.Success);
}
[Test]
public void TestCallResultError()
{
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
[Test]
public void TestCallResultSuccess()
{
var result = new CallResult<object>(new object());
ClassicAssert.IsNull(result.Error);
ClassicAssert.IsNotNull(result.Data);
Assert.That(result);
Assert.That(result.Success);
}
[Test]
public void TestCallResultSuccessAs()
{
var result = new CallResult<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 TestObject2 InnerData;
public TestObjectResult()
{
InnerData = new TestObject2();
}
}
public class TestObject2
{
}
}

View File

@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp5.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"></packagereference>
<PackageReference Include="Moq" Version="4.16.1" />
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
<packagereference Include="NUnit3TestAdapter" Version="3.17.0"></packagereference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0"></PackageReference>
</ItemGroup>
<ItemGroup>

View File

@ -16,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
{
var result = ExchangeHelpers.ClampValue(min, max, input);
Assert.AreEqual(expected, result);
Assert.That(expected == result);
}
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
@ -30,10 +30,11 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
Assert.AreEqual(expected, result);
Assert.That(expected == result);
}
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
Assert.AreEqual(expected, result);
Assert.That(expected == result);
}
[TestCase(5, 0.1563158, 0.15631)]
@ -59,7 +60,7 @@ namespace CryptoExchange.Net.UnitTests
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
{
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
Assert.AreEqual(expected, result);
Assert.That(expected == result);
}
[TestCase(0.1234560000, "0.123456")]
@ -67,7 +68,22 @@ namespace CryptoExchange.Net.UnitTests
public void NormalizeTests(decimal input, string expected)
{
var result = ExchangeHelpers.Normalize(input);
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
}
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
}
}

View File

@ -0,0 +1,163 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class OptionsTests
{
[TearDown]
public void Init()
{
TestClientOptions.Default = new TestClientOptions
{
};
}
[TestCase(null, null)]
[TestCase("", "")]
[TestCase("test", null)]
[TestCase("test", "")]
[TestCase(null, "test")]
[TestCase("", "test")]
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
{
// arrange
// act
// assert
Assert.Throws(typeof(ArgumentException),
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
}
[Test]
public void TestBasicOptionsAreSet()
{
// arrange, act
var options = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
ReceiveWindow = TimeSpan.FromSeconds(10)
};
// assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key == "123");
Assert.That(options.ApiCredentials.Secret == "456");
}
[Test]
public void TestApiOptionsAreSet()
{
// 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 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() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
[Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault()
{
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient(options =>
{
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
options.Environment = new TestEnvironment("Test", "https://test.test");
});
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "333");
Assert.That(authProvider1.GetSecret() == "444");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
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>
{
/// <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;
}
}
}

View File

@ -1,17 +1,18 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.RateLimiter;
using Microsoft.Extensions.Logging;
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;
namespace CryptoExchange.Net.UnitTests
{
@ -24,14 +25,14 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
// act
var result = client.Request<TestObject>().Result;
var result = client.Api1.Request<TestObject>().Result;
// assert
Assert.IsTrue(result.Success);
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
@ -42,62 +43,60 @@ namespace CryptoExchange.Net.UnitTests
client.SetResponse("{\"property\": 123", out _);
// act
var result = client.Request<TestObject>().Result;
var result = client.Api1.Request<TestObject>().Result;
// assert
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public void ReceivingErrorCode_Should_ResultInError()
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
// act
var result = client.Request<TestObject>().Result;
var result = await client.Api1.Request<TestObject>();
// assert
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public void ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
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 = client.Request<TestObject>().Result;
var result = await client.Api1.Request<TestObject>();
// assert
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
Assert.IsTrue(result.Error is ServerError);
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
Assert.IsTrue(result.Error.Message.Contains("123"));
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public void ReceivingErrorAndParsingError_Should_ResultInParsedError()
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 = client.Request<TestObject>().Result;
var result = await client.Api2.Request<TestObject>();
// assert
Assert.IsFalse(result.Success);
Assert.IsTrue(result.Error != null);
Assert.IsTrue(result.Error is ServerError);
Assert.IsTrue(result.Error.Code == 123);
Assert.IsTrue(result.Error.Message == "Invalid request");
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]
@ -105,20 +104,16 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
// act
var client = new TestRestClient(new RestClientOptions("")
{
BaseAddress = "http://test.address.com",
RateLimiters = new List<IRateLimiter>{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))},
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
RequestTimeout = TimeSpan.FromMinutes(1)
});
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.IsTrue(client.BaseAddress == "http://test.address.com/");
Assert.IsTrue(client.RateLimiters.Count() == 1);
Assert.IsTrue(client.RateLimitBehaviour == RateLimitingBehaviour.Fail);
Assert.IsTrue(client.RequestTimeout == TimeSpan.FromMinutes(1));
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
@ -132,16 +127,13 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
// act
var client = new TestRestClient(new RestClientOptions("")
{
BaseAddress = "http://test.address.com",
});
var client = new TestRestClient();
client.SetParameterPosition(new HttpMethod(method), pos);
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
client.SetResponse("{}", out var request);
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
@ -152,93 +144,239 @@ namespace CryptoExchange.Net.UnitTests
});
// assert
Assert.AreEqual(request.Method, new HttpMethod(method));
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
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]
public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests()
[TestCase(1, 0.1)]
[TestCase(2, 0.1)]
[TestCase(5, 1)]
[TestCase(1, 2)]
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
{
// arrange
var client = new TestRestClient(new RestClientOptions("")
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
var triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
RateLimitingBehaviour = RateLimitingBehaviour.Fail
});
client.SetResponse("{\"property\": 123}", out _);
// act
var result1 = client.Request<TestObject>().Result;
client.SetResponse("{\"property\": 123}", out _);
var result2 = client.Request<TestObject>().Result;
// assert
Assert.IsTrue(result1.Success);
Assert.IsFalse(result2.Success);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
[TestCase]
public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests()
[TestCase("/sapi/test1", true)]
[TestCase("/sapi/test2", true)]
[TestCase("/api/test1", false)]
[TestCase("sapi/test1", true)]
[TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{
// arrange
var client = new TestRestClient(new RestClientOptions("")
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
RateLimitingBehaviour = RateLimitingBehaviour.Wait
});
client.SetResponse("{\"property\": 123}", out _);
// act
var sw = Stopwatch.StartNew();
var result1 = client.Request<TestObject>().Result;
client.SetResponse("{\"property\": 123}", out _); // reset response stream
var result2 = client.Request<TestObject>().Result;
sw.Stop();
// assert
Assert.IsTrue(result1.Success);
Assert.IsTrue(result2.Success);
Assert.IsTrue(sw.ElapsedMilliseconds > 900, $"Actual: {sw.ElapsedMilliseconds}");
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
[TestCase]
public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey()
[TestCase("/sapi/", "/sapi/", true)]
[TestCase("/sapi/test", "/sapi/test", true)]
[TestCase("/sapi/test", "/sapi/test123", false)]
[TestCase("/sapi/test", "/sapi/", false)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
{
// arrange
var client = new TestRestClient(new RestClientOptions("")
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimiting ? evnt != null : evnt == null);
}
[TestCase(1, 0.1)]
[TestCase(2, 0.1)]
[TestCase(5, 1)]
[TestCase(1, 2)]
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
{
RateLimiters = new List<IRateLimiter> { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) },
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
LogLevel = LogLevel.Debug,
ApiCredentials = new ApiCredentials("TestKey", "TestSecret")
});
client.SetResponse("{\"property\": 123}", out _);
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
bool triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
// act
var sw = Stopwatch.StartNew();
var result1 = client.Request<TestObject>().Result;
client.SetKey("TestKey2", "TestSecret2"); // set to different key
client.SetResponse("{\"property\": 123}", out _); // reset response stream
var result2 = client.Request<TestObject>().Result;
client.SetKey("TestKey", "TestSecret"); // set back to original key, should delay
client.SetResponse("{\"property\": 123}", out _); // reset response stream
var result3 = client.Request<TestObject>().Result;
sw.Stop();
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);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
// assert
Assert.IsTrue(result1.Success);
Assert.IsTrue(result2.Success);
Assert.IsTrue(result3.Success);
Assert.IsTrue(sw.ElapsedMilliseconds > 900 && sw.ElapsedMilliseconds < 1900, $"Actual: {sw.ElapsedMilliseconds}");
[TestCase("/", false)]
[TestCase("/sapi/test", true)]
[TestCase("/sapi/test/123", false)]
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
[TestCase("/", false)]
[TestCase("/sapi/test", true)]
[TestCase("/sapi/test2", true)]
[TestCase("/sapi/test23", false)]
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[TestCase("/sapi/test", "/sapi/test", true)]
[TestCase("/sapi/test1", "/api/test2", true)]
[TestCase("/", "/sapi/test2", true)]
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[TestCase("https://test.com", "https://test.com", true)]
[TestCase("https://test2.com", "https://test.com", false)]
[TestCase("https://test.com", "https://test2.com", false)]
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
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);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[Test]
public async Task ConnectionRateLimiterCancel()
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
}
}

View File

@ -1,187 +1,234 @@
using System;
using System.Threading;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
//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(new SocketClientOptions("")
{
BaseAddress = "http://test.address.com",
ReconnectInterval = TimeSpan.FromSeconds(6)
});
//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);
// }
//assert
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
Assert.IsTrue(client.ReconnectInterval.TotalSeconds == 6);
}
// [TestCase(true)]
// [TestCase(false)]
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
// {
// //arrange
// var client = new TestSocketClient();
// var socket = client.CreateSocket();
// socket.CanConnect = canConnect;
[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, ""));
//act
var connectResult = client.ConnectSocketSub(new SocketConnection(client, socket));
// //assert
// Assert.That(connectResult.Success == canConnect);
// }
//assert
Assert.IsTrue(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;
[TestCase]
public void SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(client, socket);
var rstEvent = new ManualResetEvent(false);
JToken result = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
{
result = messageEvent.JsonData;
rstEvent.Set();
}));
client.ConnectSocketSub(sub);
// client.SubClient.ConnectSocketSub(sub);
// act
socket.InvokeMessage("{\"property\": 123}");
rstEvent.WaitOne(1000);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// result = messageEvent.Data;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// assert
Assert.IsTrue((int)result["property"] == 123);
}
// // act
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
// rstEvent.WaitOne(1000);
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(client, socket);
var rstEvent = new ManualResetEvent(false);
string original = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
}));
client.ConnectSocketSub(sub);
// // assert
// Assert.That(result["property"] == "123");
// }
// act
socket.InvokeMessage("{\"property\": 123}");
rstEvent.WaitOne(1000);
// [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;
// assert
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : 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" });
[TestCase]
public void DisconnectedSocket_Should_Reconnect()
{
// arrange
bool reconnected = false;
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(client, socket);
sub.ShouldReconnect = true;
client.ConnectSocketSub(sub);
var rstEvent = new ManualResetEvent(false);
sub.ConnectionRestored += (a) =>
{
reconnected = true;
rstEvent.Set();
};
// // act
// socket.InvokeMessage(msgToSend);
// rstEvent.WaitOne(1000);
// act
socket.InvokeClose();
rstEvent.WaitOne(1000);
// // assert
// Assert.That(original == (enabled ? msgToSend : null));
// }
// assert
Assert.IsTrue(reconnected);
}
// [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);
[TestCase()]
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(client, socket);
client.ConnectSocketSub(sub);
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
// 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();
// // act
// client.UnsubscribeAsync(ups).Wait();
// assert
Assert.IsTrue(socket.Connected == false);
}
// // assert
// Assert.That(socket.Connected == false);
// }
[TestCase()]
public void UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket1 = client.CreateSocket();
var socket2 = client.CreateSocket();
socket1.CanConnect = true;
socket2.CanConnect = true;
var sub1 = new SocketConnection(client, socket1);
var sub2 = new SocketConnection(client, socket2);
client.ConnectSocketSub(sub1);
client.ConnectSocketSub(sub2);
// [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) => { });
// act
client.UnsubscribeAllAsync().Wait();
// sub1.AddSubscription(subscription1);
// sub2.AddSubscription(subscription2);
// var ups1 = new UpdateSubscription(sub1, subscription1);
// var ups2 = new UpdateSubscription(sub2, subscription2);
// assert
Assert.IsTrue(socket1.Connected == false);
Assert.IsTrue(socket2.Connected == false);
}
// // act
// client.UnsubscribeAllAsync().Wait();
[TestCase()]
public void FailingToConnectSocket_Should_ReturnError()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.CanConnect = false;
var sub = new SocketConnection(client, socket);
// // assert
// Assert.That(socket1.Connected == false);
// Assert.That(socket2.Connected == false);
// }
// act
var connectResult = client.ConnectSocketSub(sub);
// [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, "");
// assert
Assert.IsFalse(connectResult.Success);
}
}
}
// // 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);
// }
// }
//}

View File

@ -1,33 +1,36 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.OrderBook;
using CryptoExchange.Net.Sockets;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class SymbolOrderBookTests
{
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions("Test", true, false);
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
private class TestableSymbolOrderBook : SymbolOrderBook
{
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
{
Initialize(_defaultOrderBookOptions);
}
public override void Dispose() {}
protected override Task<CallResult<bool>> DoResyncAsync()
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
{
throw new NotImplementedException();
}
protected override Task<CallResult<UpdateSubscription>> DoStartAsync()
protected override Task<CallResult<UpdateSubscription>> DoStartAsync(CancellationToken ct)
{
throw new NotImplementedException();
}
@ -35,12 +38,12 @@ namespace CryptoExchange.Net.UnitTests
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
Status = OrderBookStatus.Synced;
base.bids.Clear();
base._bids.Clear();
foreach (var bid in bids)
base.bids.Add(bid.Price, bid);
base.asks.Clear();
base._bids.Add(bid.Price, bid);
base._asks.Clear();
foreach (var ask in asks)
base.asks.Add(ask.Price, ask);
base._asks.Add(ask.Price, ask);
}
}
@ -54,31 +57,31 @@ namespace CryptoExchange.Net.UnitTests
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
{
var symbolOrderBook = new TestableSymbolOrderBook();
Assert.IsNotNull(symbolOrderBook.BestBid);
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
Assert.That(0m == symbolOrderBook.BestBid.Price);
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
}
[TestCase]
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
{
var symbolOrderBook = new TestableSymbolOrderBook();
Assert.IsNotNull(symbolOrderBook.BestBid);
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
Assert.That(0m == symbolOrderBook.BestBid.Price);
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
}
[TestCase]
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
{
var symbolOrderBook = new TestableSymbolOrderBook();
Assert.IsNotNull(symbolOrderBook.BestOffers);
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
}
[TestCase]
@ -101,12 +104,40 @@ namespace CryptoExchange.Net.UnitTests
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
Assert.True(resultBids.Success);
Assert.True(resultAsks.Success);
Assert.AreEqual(1.05m, resultBids.Data);
Assert.AreEqual(1.25m, resultAsks.Data);
Assert.AreEqual(1.06666667m, resultBids2.Data);
Assert.AreEqual(1.23333333m, resultAsks2.Data);
Assert.That(resultBids.Success);
Assert.That(resultAsks.Success);
Assert.That(1.05m == resultBids.Data);
Assert.That(1.25m == resultAsks.Data);
Assert.That(1.06666667m == resultBids2.Data);
Assert.That(1.23333333m == resultAsks2.Data);
}
[TestCase]
public void CalculateTradableAmount()
{
var orderbook = new TestableSymbolOrderBook();
orderbook.SetData(new List<ISymbolOrderBookEntry>
{
new BookEntry{ Price = 1, Quantity = 1 },
new BookEntry{ Price = 1.1m, Quantity = 1 },
},
new List<ISymbolOrderBookEntry>()
{
new BookEntry{ Price = 1.2m, Quantity = 1 },
new BookEntry{ Price = 1.3m, Quantity = 1 },
});
var resultBids = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Bid);
var resultAsks = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Ask);
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
Assert.That(resultBids.Success);
Assert.That(resultAsks.Success);
Assert.That(1.9m == resultBids.Data);
Assert.That(1.61538462m == resultAsks.Data);
Assert.That(1.4m == resultBids2.Data);
Assert.That(1.23076923m == resultAsks2.Data);
}
}
}

View File

@ -0,0 +1,440 @@
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
{
}
}

View File

@ -1,56 +1,99 @@
using System.Collections.Generic;
using System.Net.Http;
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 TestBaseClient(): base("Test", new RestClientOptions("http://testurl.url"), null)
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(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
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)
{
log.Write(verbosity, 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)
{
return Deserialize<T>(data, false);
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;
}
public string FillParameters(string path, params string[] values)
{
return FillPathParameter(path, values);
}
/// <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 Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
return base.AddAuthenticationToHeaders(uri, method, parameters, signed, postParameters, arraySerialization);
}
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
{
return base.AddAuthenticationToParameters(uri, method, parameters, signed, postParameters, arraySerialization);
public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret;
}
public override string Sign(string toSign)
public class TestEnvironment : TradeEnvironment
{
return toSign;
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
}

View File

@ -1,12 +1,14 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestObject
{
[JsonProperty("other")]
[JsonPropertyName("other")]
public string StringData { get; set; }
[JsonPropertyName("intData")]
public int IntData { get; set; }
[JsonPropertyName("decimalData")]
public decimal DecimalData { get; set; }
}
}

View File

@ -1,7 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Moq;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Net;
@ -12,29 +11,34 @@ 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: RestClient
public class TestRestClient: BaseRestClient
{
public TestRestClient() : base("Test", new RestClientOptions("http://testurl.url"), null)
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
public TestRestClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
Initialize(options.Value);
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
{
ParameterPositions[method] = position;
}
public void SetKey(string key, string secret)
{
SetAuthenticationProvider(new UnitTests.TestAuthProvider(new ApiCredentials(key, secret)));
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)
@ -46,21 +50,30 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new Dictionary<string, IEnumerable<string>>();
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 List<string> { val }));
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(RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
.Callback<HttpMethod, string, int>((method, uri, id) =>
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(new Uri(uri));
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);
@ -73,10 +86,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
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(RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
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);
}
@ -89,47 +109,126 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new Dictionary<string, IEnumerable<string>>();
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(key, new List<string> { val }));
request.Setup(c => c.GetHeaders()).Returns(headers);
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(RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
.Callback<HttpMethod, string, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(new Uri(uri)))
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 async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
}
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
public class TestRestApi1Client : RestApiClient
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
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 TestAuthProvider : AuthenticationProvider
public class TestRestApi2Client : RestApiClient
{
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
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() { }
public ParseErrorTestRestClient(RestClientOptions exchangeOptions) : base(exchangeOptions) { }
protected override Error ParseErrorResponse(JToken error)
{
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
}
}
}

View File

@ -0,0 +1,29 @@
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)));
}
}
}

View File

@ -1,115 +0,0 @@
using System;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestSocket: IWebsocket
{
public bool CanConnect { get; set; }
public bool Connected { get; set; }
public event Action OnClose;
public event Action<string> OnMessage;
public event Action<Exception> OnError;
public event Action OnOpen;
public int Id { get; }
public bool ShouldReconnect { get; set; }
public TimeSpan Timeout { get; set; }
public Func<string, string> DataInterpreterString { get; set; }
public Func<byte[], string> DataInterpreterBytes { get; set; }
public DateTime? DisconnectTime { get; set; }
public string Url { get; }
public bool IsClosed => !Connected;
public bool IsOpen => Connected;
public bool PingConnection { get; set; }
public TimeSpan PingInterval { get; set; }
public SslProtocols SSLProtocols { get; set; }
public Encoding Encoding { get; set; }
public int ConnectCalls { get; private set; }
public bool Reconnecting { get; set; }
public string Origin { get; set; }
public int? RatelimitPerSecond { get; set; }
public double IncomingKbps => throw new NotImplementedException();
public static int lastId = 0;
public static object lastIdLock = new object();
public TestSocket()
{
lock (lastIdLock)
{
Id = lastId + 1;
lastId++;
}
}
public Task<bool> ConnectAsync()
{
Connected = CanConnect;
ConnectCalls++;
if (CanConnect)
InvokeOpen();
return Task.FromResult(CanConnect);
}
public void Send(string data)
{
if(!Connected)
throw new Exception("Socket not connected");
}
public void Reset()
{
}
public Task CloseAsync()
{
Connected = false;
DisconnectTime = DateTime.UtcNow;
OnClose?.Invoke();
return Task.FromResult(0);
}
public void SetProxy(string host, int port)
{
throw new NotImplementedException();
}
public void Dispose()
{
}
public void InvokeClose()
{
Connected = false;
DisconnectTime = DateTime.UtcNow;
OnClose?.Invoke();
}
public void InvokeOpen()
{
OnOpen?.Invoke();
}
public void InvokeMessage(string data)
{
OnMessage?.Invoke(data);
}
public void SetProxy(ApiProxy proxy)
{
throw new NotImplementedException();
}
public void InvokeError(Exception error)
{
OnError?.Invoke(error);
}
}
}

View File

@ -1,66 +0,0 @@
using System;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Moq;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestSocketClient: SocketClient
{
public TestSocketClient() : this(new SocketClientOptions("http://testurl.url"))
{
}
public TestSocketClient(SocketClientOptions exchangeOptions) : base("test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
{
SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
}
public TestSocket CreateSocket()
{
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
return (TestSocket)CreateSocket(BaseAddress);
}
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub).Result;
}
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
{
throw new NotImplementedException();
}
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
out CallResult<object> callResult)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(JToken message, object request)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(JToken message, string identifier)
{
return true;
}
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
{
throw new NotImplementedException();
}
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,25 +0,0 @@
using Microsoft.Extensions.Logging;
using System;
using System.Text;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestStringLogger : ILogger
{
StringBuilder _builder = new StringBuilder();
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_builder.AppendLine(formatter(state, exception));
}
public string GetLogs()
{
return _builder.ToString();
}
}
}

View File

@ -0,0 +1,17 @@
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
{
}
}

View File

@ -1,12 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2008
# Visual Studio Version 17
VisualStudioVersion = 17.0.32014.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net.UnitTests", "CryptoExchange.Net.UnitTests\CryptoExchange.Net.UnitTests.csproj", "{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\BlazorClient\BlazorClient.csproj", "{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -21,10 +31,31 @@ Global
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.Build.0 = Release|Any CPU
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.Build.0 = Release|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
EndGlobalSection

View File

@ -1 +1,6 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}

View File

@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{
}

View File

@ -1,11 +0,0 @@
using System;
namespace CryptoExchange.Net.Attributes
{
/// <summary>
/// Marks property as optional
/// </summary>
public class JsonOptionalPropertyAttribute : Attribute
{
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace CryptoExchange.Net.Attributes
{
/// <summary>
/// Map a enum entry to string values
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class MapAttribute : Attribute
{
/// <summary>
/// Values mapping to the enum entry
/// </summary>
public string[] Values { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="maps"></param>
public MapAttribute(params string[] maps)
{
Values = maps;
}
}
}

View File

@ -1,4 +1,4 @@
#if !NETSTANDARD2_1
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
using System;

View File

@ -1,128 +1,101 @@
using System;
using System.IO;
using System.Security;
using System.Text;
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Api credentials info
/// Api credentials, used to sign requests accessing private endpoints
/// </summary>
public class ApiCredentials: IDisposable
public class ApiCredentials
{
/// <summary>
/// The api key to authenticate requests
/// The api key / label to authenticate requests
/// </summary>
public SecureString? Key { get; }
public string Key { get; set; }
/// <summary>
/// The api secret to authenticate requests
/// The api secret or private key to authenticate requests
/// </summary>
public SecureString? Secret { get; }
public string Secret { get; set; }
/// <summary>
/// The private key to authenticate requests
/// The api passphrase. Not needed on all exchanges
/// </summary>
public PrivateKey? PrivateKey { get; }
public string? Pass { get; set; }
/// <summary>
/// Create Api credentials providing a private key for authentication
/// Type of the credentials
/// </summary>
/// <param name="privateKey">The private key used for signing</param>
public ApiCredentials(PrivateKey privateKey)
{
PrivateKey = privateKey;
}
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(SecureString key, SecureString secret)
{
if (key == null || secret == null)
throw new ArgumentException("Key and secret can't be null/empty");
Key = key;
Secret = secret;
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(string key, string secret)
/// <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");
Key = key.ToSecureString();
Secret = secret.ToSecureString();
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>
/// Copy the credentials
/// </summary>
/// <returns></returns>
public ApiCredentials Copy()
public virtual ApiCredentials Copy()
{
if (PrivateKey == null)
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
else
return new ApiCredentials(PrivateKey!.Copy());
}
/// <summary>
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
/// </summary>
/// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true);
var stringData = reader.ReadToEnd();
var jsonData = stringData.ToJToken();
if(jsonData == null)
throw new ArgumentException("Input stream not valid json data");
var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key.ToSecureString();
Secret = secret.ToSecureString();
inputStream.Seek(0, SeekOrigin.Begin);
}
/// <summary>
/// Try get the value of a key from a JToken
/// </summary>
/// <param name="data"></param>
/// <param name="key"></param>
/// <returns></returns>
protected string? TryGetValue(JToken data, string key)
{
if (data[key] == null)
return null;
return (string) data[key]!;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Secret?.Dispose();
PrivateKey?.Dispose();
return new ApiCredentials(Key, Secret, Pass, CredentialType);
}
}
}

View File

@ -0,0 +1,25 @@
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
}
}

View File

@ -1,6 +1,16 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
#if NET8_0_OR_GREATER
using NSec.Cryptography;
#endif
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Linq;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace CryptoExchange.Net.Authentication
{
@ -9,10 +19,38 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
public abstract class AuthenticationProvider
{
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
/// <summary>
/// The provided credentials
/// The supported credential types
/// </summary>
public ApiCredentials Credentials { get; }
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
/// <summary>
/// Provided credentials
/// </summary>
protected internal readonly ApiCredentials _credentials;
/// <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
@ -20,72 +58,471 @@ namespace CryptoExchange.Net.Authentication
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
Credentials = credentials;
}
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
/// <summary>
/// Add authentication to the parameter list based on the provided credentials
/// </summary>
/// <param name="uri">The uri the request is for</param>
/// <param name="method">The HTTP method of the request</param>
/// <param name="parameters">The provided parameters for the request</param>
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
/// <param name="parameterPosition">Where parameters are placed, in the URI or in the request body</param>
/// <param name="arraySerialization">How array parameters are serialized</param>
/// <returns>Should return the original parameter list including any authentication parameters needed</returns>
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
{
return parameters;
#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>
/// Add authentication to the header dictionary based on the provided credentials
/// Authenticate a request
/// </summary>
/// <param name="uri">The uri the request is for</param>
/// <param name="method">The HTTP method of the request</param>
/// <param name="parameters">The provided parameters for the request</param>
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
/// <param name="parameterPosition">Where post parameters are placed, in the URI or in the request body</param>
/// <param name="arraySerialization">How array parameters are serialized</param>
/// <returns>Should return a dictionary containing any header key/value pairs needed for authenticating the request</returns>
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
{
return new Dictionary<string, string>();
}
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// Sign a string
/// SHA256 sign the data and return the bytes
/// </summary>
/// <param name="toSign"></param>
/// <param name="data"></param>
/// <returns></returns>
public virtual string Sign(string toSign)
protected static byte[] SignSHA256Bytes(string data)
{
return toSign;
using var encryptor = SHA256.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// Sign a byte array
/// SHA256 sign the data and return the bytes
/// </summary>
/// <param name="toSign"></param>
/// <param name="data"></param>
/// <returns></returns>
public virtual byte[] Sign(byte[] toSign)
protected static byte[] SignSHA256Bytes(byte[] data)
{
return toSign;
using var encryptor = SHA256.Create();
return encryptor.ComputeHash(data);
}
/// <summary>
/// Convert byte array to hex
/// SHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA256(string data, SignOutputType? outputType = null)
{
using var encryptor = SHA256.Create();
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = SHA256.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA384(string data, SignOutputType? outputType = null)
{
using var encryptor = SHA384.Create();
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = SHA384.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA384Bytes(string data)
{
using var encryptor = SHA384.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// SHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA384Bytes(byte[] data)
{
using var encryptor = SHA384.Create();
return encryptor.ComputeHash(data);
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA512(string data, SignOutputType? outputType = null)
{
using var encryptor = SHA512.Create();
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = SHA512.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA512Bytes(string data)
{
using var encryptor = SHA512.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// SHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignSHA512Bytes(byte[] data)
{
using var encryptor = SHA512.Create();
return encryptor.ComputeHash(data);
}
/// <summary>
/// MD5 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignMD5(string data, SignOutputType? outputType = null)
{
using var encryptor = MD5.Create();
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// MD5 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = MD5.Create();
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// MD5 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <returns></returns>
protected static byte[] SignMD5Bytes(string data)
{
using var encryptor = MD5.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA256(_sBytes);
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA384(_sBytes);
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
{
using var encryptor = new HMACSHA512(_sBytes);
var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// SHA256 sign the data
/// </summary>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
{
using var rsa = CreateRSA();
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>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
{
using var rsa = CreateRSA();
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>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
{
using var rsa = CreateRSA();
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);
}
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(string data, SignOutputType? outputType = null)
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
{
#if NET8_0_OR_GREATER
if (Ed25519Key == null)
{
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 outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
#else
throw new InvalidOperationException();
#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 ByteToString(byte[] buff)
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"); /* hex format */
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)
{
}
}
}

View File

@ -1,110 +0,0 @@
using System;
using System.Security;
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Private key info
/// </summary>
public class PrivateKey : IDisposable
{
/// <summary>
/// The private key
/// </summary>
public SecureString Key { get; }
/// <summary>
/// The private key's pass phrase
/// </summary>
public SecureString? Passphrase { get; }
/// <summary>
/// Indicates if the private key is encrypted or not
/// </summary>
public bool IsEncrypted { get; }
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
/// <param name="passphrase">The private key's passphrase</param>
public PrivateKey(SecureString key, SecureString passphrase)
{
Key = key;
Passphrase = passphrase;
IsEncrypted = true;
}
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
/// <param name="passphrase">The private key's passphrase</param>
public PrivateKey(string key, string passphrase)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
throw new ArgumentException("Key and passphrase can't be null/empty");
var secureKey = new SecureString();
foreach (var c in key)
secureKey.AppendChar(c);
secureKey.MakeReadOnly();
Key = secureKey;
var securePassphrase = new SecureString();
foreach (var c in passphrase)
securePassphrase.AppendChar(c);
securePassphrase.MakeReadOnly();
Passphrase = securePassphrase;
IsEncrypted = true;
}
/// <summary>
/// Create a private key providing an unencrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
public PrivateKey(SecureString key)
{
Key = key;
IsEncrypted = false;
}
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
public PrivateKey(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key can't be null/empty");
Key = key.ToSecureString();
IsEncrypted = false;
}
/// <summary>
/// Copy the private key
/// </summary>
/// <returns></returns>
public PrivateKey Copy()
{
if (Passphrase == null)
return new PrivateKey(Key.GetString());
else
return new PrivateKey(Key.GetString(), Passphrase.GetString());
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Passphrase?.Dispose();
}
}
}

View File

@ -0,0 +1,17 @@
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Output string type
/// </summary>
public enum SignOutputType
{
/// <summary>
/// Hex string
/// </summary>
Hex,
/// <summary>
/// Base64 string
/// </summary>
Base64
}
}

View File

@ -1,463 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net
{
/// <summary>
/// The base for all clients, websocket client and rest client
/// </summary>
public abstract class BaseClient : IDisposable
{
/// <summary>
/// The address of the client
/// </summary>
public string BaseAddress { get; }
/// <summary>
/// The name of the exchange the client is for
/// </summary>
public string ExchangeName { get; }
/// <summary>
/// The log object
/// </summary>
protected internal Log log;
/// <summary>
/// The api proxy
/// </summary>
protected ApiProxy? apiProxy;
/// <summary>
/// The authentication provider
/// </summary>
protected internal AuthenticationProvider? authProvider;
/// <summary>
/// Should check objects for missing properties based on the model and the received JSON
/// </summary>
public bool ShouldCheckObjects { get; set; }
/// <summary>
/// If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property
/// </summary>
public bool OutputOriginalData { get; private set; }
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// </summary>
protected static int lastId;
/// <summary>
/// Lock for id generating
/// </summary>
protected static object idLock = new object();
/// <summary>
/// A default serializer
/// </summary>
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture
});
/// <summary>
/// Last id used
/// </summary>
public static int LastId => lastId;
/// <summary>
/// ctor
/// </summary>
/// <param name="exchangeName">The name of the exchange this client is for</param>
/// <param name="options">The options for this client</param>
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
protected BaseClient(string exchangeName, ClientOptions options, AuthenticationProvider? authenticationProvider)
{
log = new Log(exchangeName);
authProvider = authenticationProvider;
log.UpdateWriters(options.LogWriters);
log.Level = options.LogLevel;
ExchangeName = exchangeName;
OutputOriginalData = options.OutputOriginalData;
BaseAddress = options.BaseAddress;
apiProxy = options.Proxy;
log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}");
ShouldCheckObjects = options.ShouldCheckObjects;
}
/// <summary>
/// Set the authentication provider, can be used when manually setting the API credentials
/// </summary>
/// <param name="authenticationProvider"></param>
protected void SetAuthenticationProvider(AuthenticationProvider authenticationProvider)
{
log.Write(LogLevel.Debug, "Setting api credentials");
authProvider = authenticationProvider;
}
/// <summary>
/// Tries to parse the json data and returns a JToken, validating the input not being empty and being valid json
/// </summary>
/// <param name="data">The data to parse</param>
/// <returns></returns>
protected CallResult<JToken> ValidateJson(string data)
{
if (string.IsNullOrEmpty(data))
{
var info = "Empty data object received";
log.Write(LogLevel.Error, info);
return new CallResult<JToken>(null, new DeserializeError(info, data));
}
try
{
return new CallResult<JToken>(JToken.Parse(data), null);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<JToken>(null, new DeserializeError(info, data));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<JToken>(null, new DeserializeError(info, data));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<JToken>(null, new DeserializeError(info, data));
}
}
/// <summary>
/// Deserialize a string into an object
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="data">The data to deserialize</param>
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
/// <param name="serializer">A specific serializer to use</param>
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
/// <returns></returns>
protected CallResult<T> Deserialize<T>(string data, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
{
var tokenResult = ValidateJson(data);
if (!tokenResult)
{
log.Write(LogLevel.Error, tokenResult.Error!.Message);
return new CallResult<T>(default, tokenResult.Error);
}
return Deserialize<T>(tokenResult.Data, checkObject, serializer, requestId);
}
/// <summary>
/// Deserialize a JToken into an object
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="obj">The data to deserialize</param>
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
/// <param name="serializer">A specific serializer to use</param>
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
/// <returns></returns>
protected CallResult<T> Deserialize<T>(JToken obj, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
{
if (serializer == null)
serializer = defaultSerializer;
try
{
if ((checkObject ?? ShouldCheckObjects)&& log.Level <= LogLevel.Debug)
{
// This checks the input JToken object against the class it is being serialized into and outputs any missing fields
// in either the input or the class
try
{
if (obj is JObject o)
{
CheckObject(typeof(T), o, requestId);
}
else if (obj is JArray j)
{
if (j.HasValues && j[0] is JObject jObject)
CheckObject(typeof(T).GetElementType(), jObject, requestId);
}
}
catch (Exception e)
{
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message));
}
}
return new CallResult<T>(obj.ToObject<T>(serializer), null);
}
catch (JsonReaderException jre)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
log.Write(LogLevel.Error, info);
return new CallResult<T>(default, new DeserializeError(info, obj));
}
catch (JsonSerializationException jse)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
log.Write(LogLevel.Error, info);
return new CallResult<T>(default, new DeserializeError(info, obj));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
log.Write(LogLevel.Error, info);
return new CallResult<T>(default, new DeserializeError(info, obj));
}
}
/// <summary>
/// Deserialize a stream into an object
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="stream">The stream to deserialize</param>
/// <param name="serializer">A specific serializer to use</param>
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
/// <returns></returns>
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
{
if (serializer == null)
serializer = defaultSerializer;
try
{
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
// If we have to output the original json data or output the data into the logging we'll have to read to full response
// in order to log/return the json data
if (OutputOriginalData || log.Level <= LogLevel.Debug)
{
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}");
var result = Deserialize<T>(data, null, serializer, requestId);
if(OutputOriginalData)
result.OriginalData = data;
return result;
}
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
using var jsonReader = new JsonTextReader(reader);
return new CallResult<T>(serializer.Deserialize<T>(jsonReader), null);
}
catch (JsonReaderException jre)
{
string data;
if (stream.CanSeek)
{
// If we can seek the stream rewind it so we can retrieve the original data that was sent
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Debug LogLevel]";
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
}
catch (JsonSerializationException jse)
{
string data;
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Debug LogLevel]";
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
}
catch (Exception ex)
{
string data;
if (stream.CanSeek) {
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Debug LogLevel]";
var exceptionInfo = ex.ToLogString();
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
return new CallResult<T>(default, new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
}
}
private async Task<string> ReadStreamAsync(Stream stream)
{
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
private void CheckObject(Type type, JObject obj, int? requestId = null)
{
if (type == null)
return;
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
// If type has a custom JsonConverter we assume this will handle property mapping
return;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
return;
if (!obj.HasValues && type != typeof(object))
{
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty");
return;
}
var isDif = false;
var properties = new List<string>();
var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
if (ignore != null)
continue;
var propertyName = ((JsonPropertyAttribute?) attr)?.PropertyName;
properties.Add(propertyName ?? prop.Name);
}
foreach (var token in obj)
{
var d = properties.FirstOrDefault(p => p == token.Key);
if (d == null)
{
d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase));
if (d == null)
{
if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
{
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
isDif = true;
}
continue;
}
}
properties.Remove(d);
var propType = GetProperty(d, props)?.PropertyType;
if (propType == null || token.Value == null)
continue;
if (!IsSimple(propType) && propType != typeof(DateTime))
{
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
CheckObject(propType.GetElementType()!, (JObject)token.Value[0]!, requestId);
else if (token.Value is JObject o)
CheckObject(propType, o, requestId);
}
}
foreach (var prop in properties)
{
var propInfo = props.First(p => p.Name == prop ||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
if (optional != null)
continue;
isDif = true;
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
}
if (isDif)
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj);
}
private static PropertyInfo? GetProperty(string name, IEnumerable<PropertyInfo> props)
{
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
if (attr == null)
{
if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase))
return prop;
}
else
{
if (((JsonPropertyAttribute)attr).PropertyName == name)
return prop;
}
}
return null;
}
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);
}
/// <summary>
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
/// </summary>
/// <returns></returns>
protected int NextId()
{
lock (idLock)
{
lastId += 1;
return lastId;
}
}
/// <summary>
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
/// </summary>
/// <param name="path">The total path string</param>
/// <param name="values">The values to fill</param>
/// <returns></returns>
protected static string FillPathParameter(string path, params string[] values)
{
foreach (var value in values)
{
var index = path.IndexOf("{}", StringComparison.Ordinal);
if (index >= 0)
{
path = path.Remove(index, 2);
path = path.Insert(index, value);
}
}
return path;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
authProvider?.Credentials?.Dispose();
log.Write(LogLevel.Debug, "Disposing exchange client");
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
namespace CryptoExchange.Net.Caching
{
internal class MemoryCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly object _lock = new object();
#endif
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="value">Cache value</param>
public void Add(string key, object value)
{
var cacheItem = new CacheItem(DateTime.UtcNow, value);
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
}
/// <summary>
/// Get a cached value
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="maxAge">The max age of the cached entry</param>
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
return value.Value;
}
private class CacheItem
{
public DateTime CacheTime { get; }
public object Value { get; }
public CacheItem(DateTime cacheTime, object value)
{
CacheTime = cacheTime;
Value = value;
}
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base API for all API clients
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
/// <summary>
/// Logger
/// </summary>
protected ILogger _logger;
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; }
/// <summary>
/// The environment this client communicates to
/// </summary>
public string BaseAddress { get; }
/// <summary>
/// Output the original string data along with the deserialized object
/// </summary>
public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Api options
/// </summary>
public ApiOptions ApiOptions { get; }
/// <summary>
/// Client Options
/// </summary>
public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</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="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
{
_logger = logger;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
if (ApiCredentials != 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 />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
_disposing = true;
}
}
}

View File

@ -0,0 +1,132 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// The base for all clients, websocket client and rest client
/// </summary>
public abstract class BaseClient : IDisposable
{
/// <summary>
/// Version of the CryptoExchange.Net base library
/// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{
get
{
lock(_versionLock)
{
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion;
}
}
}
/// <summary>
/// The name of the API the client is for
/// </summary>
public string Exchange { get; }
/// <summary>
/// Api clients in this client
/// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary>
/// The log object
/// </summary>
protected internal ILogger _logger;
#if NET9_0_OR_GREATER
private readonly Lock _versionLock = new Lock();
#else
private readonly object _versionLock = new object();
#endif
private Version _exchangeVersion;
/// <summary>
/// Provided client options
/// </summary>
public ExchangeOptions ClientOptions { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected BaseClient(ILoggerFactory? logger, string exchange)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
Exchange = exchange;
}
/// <summary>
/// Initialize the client with the specified options
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
protected virtual void Initialize(ExchangeOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(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>
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
{
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
foreach (var client in ApiClients)
client.Dispose();
}
}
}

View File

@ -0,0 +1,28 @@
using System.Linq;
using CryptoExchange.Net.Interfaces.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base rest client
/// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient
{
/// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
/// <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)
{
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
}
}

View File

@ -0,0 +1,137 @@
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base for socket client implementations
/// </summary>
public abstract class BaseSocketClient : BaseClient, ISocketClient
{
#region fields
/// <summary>
/// If client is disposing
/// </summary>
protected bool _disposing;
/// <inheritdoc />
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
/// <inheritdoc />
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
foreach (var socket in ApiClients.OfType<SocketApiClient>())
{
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (result)
break;
}
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
tasks.Add(client.UnsubscribeAllAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(CurrentConnections);
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
tasks.Add(client.ReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
/// <summary>
/// Returns the state of all socket api clients
/// </summary>
/// <returns></returns>
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
{
var result = new List<SocketApiClient.SocketApiClientState>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
result.Add(client.GetState());
}
return result;
}
}
}

View File

@ -0,0 +1,67 @@
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();
}
}
}

View File

@ -0,0 +1,24 @@
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)
{
}
}
}

View File

@ -0,0 +1,24 @@
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)
{
}
}
}

View File

@ -0,0 +1,777 @@
using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base rest API client for interacting with a REST API
/// </summary>
public abstract class RestApiClient : BaseApiClient, IRestApiClient
{
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
/// <summary>
/// Request body content type
/// </summary>
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
protected internal string RequestBodyEmptyContent = "{}";
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary>
/// Whether parameters need to be ordered
/// </summary>
protected internal bool OrderParameters { get; set; } = true;
/// <summary>
/// Parameter order comparer
/// </summary>
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
{
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody },
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
};
/// <inheritdoc />
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
/// <summary>
/// Memory cache
/// </summary>
private readonly static MemoryCache _cache = new MemoryCache();
/// <summary>
/// The message handler
/// </summary>
protected abstract IRestMessageHandler MessageHandler { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="httpClient">HttpClient to use</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
: base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress,
options,
apiOptions)
{
RequestFactory.Configure(options, httpClient);
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected abstract IStreamMessageAccessor CreateAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
/// <returns></returns>
protected abstract IMessageSerializer CreateSerializer();
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult> SendAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
{
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
return result.AsDataless();
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
baseAddress,
definition,
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken,
additionalHeaders,
weight,
weightSingleLimiter,
rateLimitKeySuffix);
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
var requestId = ExchangeHelpers.NextId();
if (definition.Authenticated && AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new WebCallResult<T>(new NoApiCredentialsError());
}
string? cacheKey = null;
if (ShouldCache(definition))
{
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
_logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null)
{
_logger.CacheHit(cacheKey);
var original = (WebCallResult<T>)cachedValue;
return original.Cached();
}
_logger.CacheNotHit(cacheKey);
}
int currentTry = 0;
while (true)
{
currentTry++;
var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
error = await RateLimitAsync(
baseAddress,
requestId,
definition,
weight ?? definition.Weight,
cancellationToken,
weightSingleLimiter,
rateLimitKeySuffix).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
var request = CreateRequest(
requestId,
baseAddress,
definition,
uriParameters,
bodyParameters,
additionalHeaders);
if (_logger.IsEnabled(LogLevel.Debug))
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++;
var result = await GetResponseAsync2<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (result.Error is not CancellationRequestedError)
{
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
if (!result)
{
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
}
else
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
}
}
else
{
_logger.RestApiCancellationRequested(result.RequestId);
}
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
continue;
if (result.Success &&
ShouldCache(definition))
{
_cache.Add(cacheKey!, result);
}
return result;
}
}
private async ValueTask<Error?> CheckTimeSync(int requestId, RequestDefinition definition)
{
if (!definition.Authenticated)
return null;
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeError = await syncTask.ConfigureAwait(false);
if (syncTimeError != null)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeError!.ToString());
return syncTimeError;
}
}
return null;
}
/// <summary>
/// Check rate limits for the request
/// </summary>
protected virtual async ValueTask<Error?> RateLimitAsync(
string host,
int requestId,
RequestDefinition definition,
int weight,
CancellationToken cancellationToken,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
// Rate limiting
var requestWeight = weight;
if (requestWeight != 0)
{
if (definition.RateLimitGate == null)
throw new Exception("Ratelimit gate not set when request weight is not 0");
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return limitResult.Error!;
}
}
// Endpoint specific rate limiting
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
{
if (definition.RateLimitGate == null)
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
if (ClientOptions.RateLimiterEnabled)
{
var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return limitResult.Error!;
}
}
return null;
}
/// <summary>
/// Creates a request object
/// </summary>
/// <param name="requestId">Id of the request</param>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">The query parameters of the request</param>
/// <param name="bodyParameters">The body parameters of the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
protected virtual IRequest CreateRequest(
int requestId,
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders)
{
var requestConfiguration = new RestRequestConfiguration(
definition,
baseAddress,
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
additionalHeaders,
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
try
{
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
var queryString = requestConfiguration.GetQueryString(true);
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
queryString = $"?{queryString}";
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
request.Accept = MessageHandler.AcceptHeader;
if (requestConfiguration.Headers != null)
{
foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value);
}
foreach (var header in StandardRequestHeaders)
{
// Only add it if it isn't overwritten
requestConfiguration.Headers ??= new Dictionary<string, string>();
if (!requestConfiguration.Headers.ContainsKey(header.Key))
request.AddHeader(header.Key, header.Value);
}
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
var bodyContent = requestConfiguration.GetBodyContent();
if (bodyContent != null)
{
request.SetContent(bodyContent, contentType);
}
else
{
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
}
return request;
}
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
/// <param name="requestDefinition">The request definition</param>
/// <param name="request">The request object to execute</param>
/// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
RequestDefinition requestDefinition,
IRequest request,
IRateLimitGate? gate,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
Stream? responseStream = null;
IResponse? response = null;
try
{
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
string? originalData = null;
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
{
// If we want to return the original string data from the stream, but still want to process it
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
{
memoryStream.Position = 0;
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
if (_logger.IsEnabled(LogLevel.Trace))
_logger.RestApiReceivedResponse(request.RequestId, originalData);
}
// Continue processing from the memory stream since the response stream is already read and we can't seek it
responseStream.Close();
memoryStream.Position = 0;
responseStream = memoryStream;
}
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{
// If the response status is not success it is an error by definition
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
{
// Specifically handle rate limit errors
var rateError = await MessageHandler.ParseErrorRateLimitResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
error = rateError;
}
else
{
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
try
{
error = await MessageHandler.ParseErrorResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
}
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
// Data response received, inspect the message and check if it is an error or not
var parsedError = await MessageHandler.CheckForErrorResponse(
requestDefinition,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
{
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
if (MessageHandler.RequiresSeekableStream)
// Reset stream read position as it might not be at the start if `CheckForErrorResponse` has read from it
responseStream.Position = 0;
// Try deserialization into the expected type
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
if (deserializeError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
try
{
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
}
else
{
// Request timed out
var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
}
catch (ArgumentException argumentException)
{
if (argumentException.Message.StartsWith("Only HTTP/"))
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
catch (NotSupportedException notSupportedException)
{
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
finally
{
responseStream?.Close();
response?.Close();
}
}
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
/// Note that this is always called; even when the request might be successful
/// </summary>
/// <typeparam name="T">WebCallResult type parameter</typeparam>
/// <param name="gate">The rate limit gate the call used</param>
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
{
if (tries >= 2)
// Only retry once
return false;
if (callResult.Error is ServerRateLimitError
&& ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null)
{
var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false);
if (retryTime == null)
return false;
if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60))
{
_logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value);
return true;
}
}
return false;
}
/// <summary>
/// Writes the parameters of the request to the request object body
/// </summary>
/// <param name="request">The request to set the parameters on</param>
/// <param name="parameters">The parameters to set</param>
/// <param name="contentType">The content type of the data</param>
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
{
if (contentType == Constants.JsonContentHeader)
{
var serializer = CreateSerializer();
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
// Write the parameters as json in the body
string stringData;
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = stringSerializer.Serialize(value);
else
stringData = stringSerializer.Serialize(parameters);
request.SetContent(stringData, contentType);
}
else if (contentType == Constants.FormContentHeader)
{
// Write the parameters as form data in the body
var stringData = parameters.ToFormData();
request.SetContent(stringData, contentType);
}
}
/// <summary>
/// Create the parameter IDictionary
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
{
if (!OrderParameters)
return parameters;
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
}
/// <summary>
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary>
/// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
internal async ValueTask<Error?> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return null;
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return null;
}
var localTime = DateTime.UtcNow;
var result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.Error;
}
if (TotalRequestsMade == 1)
{
// If this was the first request make another one to calculate the offset since the first one can be slower
localTime = DateTime.UtcNow;
result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.Error;
}
}
// Calculate time offset between local and server
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return null;
}
private bool ShouldCache(RequestDefinition definition)
=> ClientOptions.CachingEnabled
&& definition.Method == HttpMethod.Get
&& !definition.PreventCaching;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,200 +0,0 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Reflection;
using CryptoExchange.Net.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
public class ArrayConverter : JsonConverter
{
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
private static readonly ConcurrentDictionary<(Type, Type), Attribute> attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return true;
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (objectType == typeof(JToken))
return JToken.Load(reader);
var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType);
}
private static object? ParseObject(JArray arr, object result, Type objectType)
{
foreach (var property in objectType.GetProperties())
{
var attribute = GetCustomAttribute<ArrayPropertyAttribute>(property);
if (attribute == null)
continue;
if (attribute.Index >= arr.Count)
continue;
if (property.PropertyType.BaseType == typeof(Array))
{
var objType = property.PropertyType.GetElementType();
var innerArray = (JArray)arr[attribute.Index];
var count = 0;
if (innerArray.Count == 0)
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
property.SetValue(result, arrayResult);
}
else if (innerArray[0].Type == JTokenType.Array)
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
foreach (var obj in innerArray)
{
var innerObj = Activator.CreateInstance(objType);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType);
count++;
}
property.SetValue(result, arrayResult);
}
else
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
var innerObj = Activator.CreateInstance(objType);
arrayResult[0] = ParseObject(innerArray, innerObj, objType);
property.SetValue(result, arrayResult);
}
continue;
}
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(property.PropertyType);
object? value;
if (converterAttribute != null)
{
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
}
else if (conversionAttribute != null)
{
value = arr[attribute.Index].ToObject(property.PropertyType);
}
else
{
value = arr[attribute.Index];
}
if (value != null && property.PropertyType.IsInstanceOfType(value))
property.SetValue(result, value);
else
{
if (value is JToken token)
if (token.Type == JTokenType.Null)
value = null;
if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().Contains("e")))
{
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
property.SetValue(result, dec);
}
else
{
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
}
}
}
return result;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
return;
writer.WriteStartArray();
var props = value.GetType().GetProperties();
var ordered = props.OrderBy(p => GetCustomAttribute<ArrayPropertyAttribute>(p)?.Index);
var last = -1;
foreach (var prop in ordered)
{
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(prop);
if (arrayProp == null)
continue;
if (arrayProp.Index == last)
continue;
while (arrayProp.Index != last + 1)
{
writer.WriteValue((string?)null);
last += 1;
}
last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value));
else
writer.WriteValue(prop.GetValue(value));
}
writer.WriteEndArray();
}
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);
}
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
}
/// <summary>
/// Mark property as an index in the array
/// </summary>
public class ArrayPropertyAttribute: Attribute
{
/// <summary>
/// The index in the array
/// </summary>
public int Index { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="index"></param>
public ArrayPropertyAttribute(int index)
{
Index = index;
}
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute : Attribute
{
/// <summary>
/// The index in the array
/// </summary>
public int Index { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="index"></param>
public ArrayPropertyAttribute(int index)
{
Index = index;
}
}
}

View File

@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Base class for enum converters
/// </summary>
/// <typeparam name="T">Type of enum to convert</typeparam>
public abstract class BaseConverter<T>: JsonConverter where T: struct
{
/// <summary>
/// The enum->string mapping
/// </summary>
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
private readonly bool quotes;
/// <summary>
/// ctor
/// </summary>
/// <param name="useQuotes"></param>
protected BaseConverter(bool useQuotes)
{
quotes = useQuotes;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var stringValue = value == null? null: GetValue((T) value);
if (quotes)
writer.WriteValue(stringValue);
else
writer.WriteRawValue(stringValue);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
if (!GetValue(reader.Value.ToString(), out var result))
{
Debug.WriteLine($"Cannot map enum. Type: {typeof(T)}, Value: {reader.Value}");
return null;
}
return result;
}
/// <summary>
/// Convert a string value
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public T ReadString(string data)
{
return Mapping.FirstOrDefault(v => v.Value == data).Key;
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
// Check if it is type, or nullable of type
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
}
private bool GetValue(string value, out T result)
{
//check for exact match first, then if not found fallback to a case insensitive match
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if(mapping.Equals(default(KeyValuePair<T, string>)))
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<T, string>)))
{
result = mapping.Key;
return true;
}
result = default;
return false;
}
private string GetValue(T value)
{
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Concurrent;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Caching for JsonSerializerContext instances
/// </summary>
public static class JsonSerializerContextCache
{
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
/// <summary>
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
/// </summary>
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{
var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
var instance = new T();
_cache[contextType] = instance;
return instance;
}
}
}

View File

@ -0,0 +1,64 @@
using CryptoExchange.Net.Objects;
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// REST message handler
/// </summary>
public interface IRestMessageHandler
{
/// <summary>
/// The `accept` HTTP response header for the request
/// </summary>
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
/// <summary>
/// Whether a seekable stream is required
/// </summary>
bool RequiresSeekableStream { get; }
/// <summary>
/// Parse the response when the HTTP response status indicated an error
/// </summary>
ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Parse the response when the HTTP response status indicated a rate limit error
/// </summary>
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Check if the response is an error response; if so return the error.<br />
/// Note that if the API returns a standard result wrapper, something like this:
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
/// then the `CheckDeserializedResponse` method should be used for checking the result
/// </summary>
ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Deserialize the response stream
/// </summary>
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
Stream responseStream,
CancellationToken ct);
/// <summary>
/// Check whether the resulting T object indicates an error or not
/// </summary>
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Net.WebSockets;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// WebSocket message handler
/// </summary>
public interface ISocketMessageHandler
{
/// <summary>
/// Get an identifier for the message which can be used to determine the type of the message
/// </summary>
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
string? GetTopicFilter(object deserializedObject);
/// <summary>
/// Deserialize to the provided type
/// </summary>
object Deserialize(ReadOnlySpan<byte> data, Type type);
}
}

View File

@ -0,0 +1,46 @@
using System;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Message type definition
/// </summary>
public class MessageTypeDefinition
{
/// <summary>
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
/// </summary>
public bool ForceIfFound { get; set; }
/// <summary>
/// The fields a message needs to contain for this definition
/// </summary>
public MessageFieldReference[] Fields { get; set; } = [];
/// <summary>
/// The callback for getting the identifier string
/// </summary>
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
/// <summary>
/// The static identifier string to return when this evaluator is matched
/// </summary>
public string? StaticIdentifier { get; set; }
internal string? GetMessageType(SearchResult result)
{
if (StaticIdentifier != null)
return StaticIdentifier;
return TypeIdentifierCallback!(result);
}
internal bool Satisfied(SearchResult result)
{
foreach(var field in Fields)
{
if (!result.Contains(field))
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,15 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
internal class MessageEvalutorFieldReference
{
public bool SkipReading { get; set; }
public bool OverlappingField { get; set; }
public MessageFieldReference Field { get; set; }
public MessageTypeDefinition? ForceEvaluator { get; set; }
public MessageEvalutorFieldReference(MessageFieldReference field)
{
Field = field;
}
}
}

View File

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Reference to a message field
/// </summary>
public abstract class MessageFieldReference
{
/// <summary>
/// The name for this search field
/// </summary>
public string SearchName { get; set; }
/// <summary>
/// The depth at which to look for this field
/// </summary>
public int Depth { get; set; } = 1;
/// <summary>
/// Callback to check if the field value matches an expected constraint
/// </summary>
public Func<string?, bool>? Constraint { get; private set; }
/// <summary>
/// Check whether the value is one of the string values in the set
/// </summary>
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
{
Constraint = set.Contains;
return this;
}
/// <summary>
/// Check whether the value is equal to a string
/// </summary>
public MessageFieldReference WithEqualConstraint(string compare)
{
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not equal to a string
/// </summary>
public MessageFieldReference WithNotEqualConstraint(string compare)
{
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not null
/// </summary>
public MessageFieldReference WithNotNullConstraint()
{
Constraint = x => x != null;
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraint(string start)
{
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
{
Constraint = x =>
{
if (x == null)
return false;
foreach (var item in startValues)
{
if (x!.StartsWith(item, StringComparison.Ordinal))
return true;
}
return false;
};
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
{
Constraint = constraint;
return this;
}
/// <summary>
/// ctor
/// </summary>
public MessageFieldReference(string searchName)
{
SearchName = searchName;
}
}
/// <summary>
/// Reference to a property message field
/// </summary>
public class PropertyFieldReference : MessageFieldReference
{
/// <summary>
/// The property name in the JSON
/// </summary>
public byte[] PropertyName { get; set; }
/// <summary>
/// Whether the property value is array values
/// </summary>
public bool ArrayValues { get; set; }
/// <summary>
/// ctor
/// </summary>
public PropertyFieldReference(string propertyName) : base(propertyName)
{
PropertyName = Encoding.UTF8.GetBytes(propertyName);
}
}
/// <summary>
/// Reference to an array message field
/// </summary>
public class ArrayFieldReference : MessageFieldReference
{
/// <summary>
/// The index in the array
/// </summary>
public int ArrayIndex { get; set; }
/// <summary>
/// ctor
/// </summary>
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
{
Depth = depth;
ArrayIndex = index;
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// The results of a search for fields in a JSON message
/// </summary>
public class SearchResult
{
private List<SearchResultItem> _items = new List<SearchResultItem>();
/// <summary>
/// Get the value of a field
/// </summary>
public string? FieldValue(string searchName)
{
foreach (var item in _items)
{
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
return item.Value;
}
throw new Exception($"No field value found for {searchName}");
}
/// <summary>
/// The number of found search field values
/// </summary>
public int Count => _items.Count;
/// <summary>
/// Clear the search result
/// </summary>
public void Clear() => _items.Clear();
/// <summary>
/// Whether the value for a specific field was found
/// </summary>
public bool Contains(MessageFieldReference field)
{
foreach (var item in _items)
{
if (item.Field == field)
return true;
}
return false;
}
/// <summary>
/// Write a value to the result
/// </summary>
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
{
Field = field,
Value = value
});
}
}

View File

@ -0,0 +1,17 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Search result value
/// </summary>
public struct SearchResultItem
{
/// <summary>
/// The field the values is for
/// </summary>
public MessageFieldReference Field { get; set; }
/// <summary>
/// The value of the field
/// </summary>
public string? Value { get; set; }
}
}

View File

@ -0,0 +1,49 @@
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); }
}
}

View File

@ -0,0 +1,50 @@
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();
}
}
}

View File

@ -0,0 +1,43 @@
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;
}
}
}

View File

@ -0,0 +1,21 @@
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
}
}

View File

@ -0,0 +1,246 @@
using CryptoExchange.Net.Exceptions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
#if NET5_0_OR_GREATER
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
#else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
{
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
writer.WriteStartArray();
var last = -1;
foreach (var indexProps in _typePropertyInfo)
{
foreach (var prop in indexProps.Value)
{
if (prop.ArrayProperty.Index == last)
// Don't write the same index twice
continue;
while (prop.ArrayProperty.Index != last + 1)
{
writer.WriteNullValue();
last += 1;
}
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
}
writer.WriteEndArray();
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = new T();
return ParseObject(ref reader, result, options);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#else
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new CeDeserializationException("Not an array");
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
int index = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
{
index++;
continue;
}
foreach (var attribute in indexAttributes)
{
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverter != null)
{
if (attribute.JsonSerializerOptions == null)
{
attribute.JsonSerializerOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
}
else if (attribute.DefaultDeserialization)
{
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
index++;
}
return result;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
#else
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
#endif
{
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
if (!result.TryGetValue(att.Index, out var indexList))
{
indexList = new List<ArrayPropertyInfo>();
result[att.Index] = indexList;
}
indexList.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
return result;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
try
{
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
}
try
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
}
}

View File

@ -0,0 +1,95 @@
using Microsoft.Extensions.Logging;
using System;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Bool converter
/// </summary>
public class BoolConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
}
private class BoolConverterInnerNullable : JsonConverter<bool?>
{
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
else
writer.WriteNullValue();
}
}
private class BoolConverterInner : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options) ?? false;
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True)
return true;
if (reader.TokenType == JsonTokenType.False)
return false;
var value = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
_ => null
};
value = value?.ToLowerInvariant().Trim();
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
switch (value)
{
case "true":
case "yes":
case "y":
case "1":
case "on":
return true;
case "false":
case "no":
case "n":
case "0":
case "off":
case "-1":
return false;
}
throw new SerializationException($"Can't convert bool value {value}");
}
}
}

View File

@ -0,0 +1,35 @@
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 enum values
/// </summary>
#if NET5_0_OR_GREATER
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
#else
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
#endif
{
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
}
}
}

View File

@ -0,0 +1,273 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Date time converter
/// </summary>
public class DateTimeConverter : JsonConverterFactory
{
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
}
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (value.Value == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private class DateTimeConverterInner : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
var dtValue = value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
if (reader.TokenType is JsonTokenType.Number)
{
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
return default;
return ParseFromDecimal(decValue);
}
else if (reader.TokenType is JsonTokenType.String)
{
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
{
return default;
}
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
}
else
{
return reader.GetDateTime();
}
}
/// <summary>
/// Parse a double value to datetime
/// </summary>
public static DateTime ParseFromDouble(double value)
=> ParseFromDecimal((decimal)value);
/// <summary>
/// Parse a decimal value to datetime
/// </summary>
public static DateTime ParseFromDecimal(decimal value)
{
if (value < 19999999999)
return ConvertFromSeconds(value);
if (value < 19999999999999)
return ConvertFromMilliseconds(value);
if (value < 19999999999999999)
return ConvertFromMicroseconds(value);
return ConvertFromNanoseconds(value);
}
/// <summary>
/// Parse a string value to datetime
/// </summary>
public static DateTime ParseFromString(string stringValue, string? resolverName)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
{
// Parse 1637745563.000 format
if (decimalValue <= 0)
return default;
if (decimalValue < 19999999999)
return ConvertFromSeconds(decimalValue);
if (decimalValue < 19999999999999)
return ConvertFromMilliseconds(decimalValue);
if (decimalValue < 19999999999999999)
return ConvertFromMicroseconds(decimalValue);
return ConvertFromNanoseconds(decimalValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Decimal converter
/// </summary>
public class DecimalConverter : JsonConverter<decimal?>
{
/// <inheritdoc />
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
return ExchangeHelpers.ParseDecimal(value);
}
try
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
}
}

View File

@ -0,0 +1,359 @@
using CryptoExchange.Net.Attributes;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Static EnumConverter methods
/// </summary>
public static class EnumConverter
{
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
#else
public static T? ParseString<T>(string value) where T : struct, Enum
#endif
=> EnumConverter<T>.ParseString(value);
/// <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
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
#else
public static string GetString<T>(T enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
/// <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
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
#if NET5_0_OR_GREATER
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
#else
public static string? GetString<T>(T? enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
}
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary>
#if NET5_0_OR_GREATER
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
#else
public class EnumConverter<T>
#endif
: 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
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
private static FrozenDictionary<T, string>? _mappingToString = null;
#else
private static List<EnumMapping>? _mappingToEnum = null;
private static Dictionary<T, string>? _mappingToString = null;
#endif
private NullableEnumConverter? _nullableEnumConverter = null;
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
internal class NullableEnumConverter : JsonConverter<T?>
{
private readonly EnumConverter<T> _enumConverter;
public NullableEnumConverter(EnumConverter<T> enumConverter)
{
_enumConverter = enumConverter;
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
}
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
_enumConverter.Write(writer, value.Value, options);
}
}
}
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
if (t == null)
{
if (isEmptyString && !_unknownValuesWarned.Contains(null))
{
// We received an empty string and have no mapping for it, and the property isn't nullable
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
}
else
{
return t.Value;
}
}
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
{
isEmptyString = false;
var enumType = typeof(T);
if (_mappingToEnum == null)
CreateMapping();
var stringValue = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt32().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (stringValue is null)
return null;
if (!GetValue(enumType, stringValue, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{
isEmptyString = true;
}
else
{
// We received an enum value but weren't able to parse it.
if (!_unknownValuesWarned.Contains(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");
}
}
return null;
}
return result;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var stringValue = GetString(value);
writer.WriteStringValue(stringValue);
}
private static bool GetValue(Type objectType, string value, out T? result)
{
if (_mappingToEnum != null)
{
EnumMapping? mapping = null;
// Try match on full equals
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
{
result = mapping.Value;
return true;
}
}
if (objectType.IsDefined(typeof(FlagsAttribute)))
{
var intValue = int.Parse(value);
result = (T)Enum.ToObject(objectType, intValue);
return true;
}
if (_unknownValuesWarned.Contains(value))
{
// Check if it is an known unknown value
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
result = default;
return false;
}
if (String.IsNullOrEmpty(value))
{
// An empty/null value will always fail when parsing, so just return here
result = default;
return false;
}
try
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
private static void CreateMapping()
{
var mappingToEnum = new List<EnumMapping>();
var mappingToString = new Dictionary<T, string>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
var enumMembers = enumType.GetFields();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
{
var enumVal = (T)Enum.Parse(enumType, member.Name);
mappingToEnum.Add(new EnumMapping(enumVal, value));
if (!mappingToString.ContainsKey(enumVal))
mappingToString.Add(enumVal, value);
}
}
}
#if NET8_0_OR_GREATER
_mappingToEnum = mappingToEnum.ToFrozenSet();
_mappingToString = mappingToString.ToFrozenDictionary();
#else
_mappingToEnum = mappingToEnum;
_mappingToString = mappingToString;
#endif
}
/// <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
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(T? enumValue)
{
if (_mappingToString == null)
CreateMapping();
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString());
}
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString(string value)
{
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mappingToEnum == null)
CreateMapping();
EnumMapping? mapping = null;
// Try match on full equals
foreach(var item in _mappingToEnum!)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
return mapping.Value;
try
{
// If no explicit mapping is found try to parse string
return (T)Enum.Parse(type, value, true);
}
catch (Exception)
{
return default;
}
}
/// <inheritdoc />
public JsonConverter CreateNullableConverter()
{
_nullableEnumConverter ??= new NullableEnumConverter(this);
return _nullableEnumConverter;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for serializing enum values as int
/// </summary>
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
{
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> writer.WriteNumberValue((int)(object)value);
}
}

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal interface INullableConverterFactory
{
JsonConverter CreateNullableConverter();
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Int converter
/// </summary>
public class IntConverter : JsonConverter<int?>
{
/// <inheritdoc />
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
return reader.GetInt32();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Int converter
/// </summary>
public class LongConverter : JsonConverter<long?>
{
/// <inheritdoc />
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
return reader.GetInt64();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
}

View File

@ -0,0 +1,117 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON REST message handler
/// </summary>
public abstract class JsonRestMessageHandler : IRestMessageHandler
{
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
/// <summary>
/// Empty rate limit error
/// </summary>
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
/// <inheritdoc />
public virtual bool RequiresSeekableStream => false;
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <inheritdoc />
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
/// <inheritdoc />
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
if (DateTime.TryParse(value, out var datetime))
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
}
/// <inheritdoc />
public abstract ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <inheritdoc />
public virtual ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream) => new ValueTask<Error?>((Error?)null);
/// <summary>
/// Read the response into a JsonDocument object
/// </summary>
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
{
try
{
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
return (null, document);
}
catch (Exception ex)
{
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
{
try
{
#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.
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
return (result, null);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return (default, new DeserializeError(info, ex));
}
catch (Exception ex)
{
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
}
}

View File

@ -0,0 +1,348 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
/// </summary>
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
{
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <summary>
/// Message evaluators
/// </summary>
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
private readonly SearchResult _searchResult = new();
private bool _hasArraySearches;
private bool _initialized;
private int _maxSearchDepth;
private MessageTypeDefinition? _topEvaluator;
private List<MessageEvalutorFieldReference>? _searchFields;
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
private Dictionary<Type, Func<object, string?>>? _mapping;
/// <summary>
/// Add a mapping of a specific object of a type to a specific topic
/// </summary>
/// <typeparam name="T">Type to get topic for</typeparam>
/// <param name="mapping">The topic retrieve delegate</param>
protected void AddTopicMapping<T>(Func<T, string?> mapping)
{
_mapping ??= new Dictionary<Type, Func<object, string?>>();
_mapping.Add(typeof(T), x => mapping((T)x));
}
private void InitializeConverter()
{
if (_initialized)
return;
_maxSearchDepth = int.MinValue;
_searchFields = new List<MessageEvalutorFieldReference>();
foreach (var evaluator in TypeEvaluators)
{
_topEvaluator ??= evaluator;
foreach (var field in evaluator.Fields)
{
var overlapping = _searchFields.Where(otherField =>
{
if (field is PropertyFieldReference propRef
&& otherField.Field is PropertyFieldReference otherPropRef)
{
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
}
else if (field is ArrayFieldReference arrayRef
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
{
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
}
return false;
}).ToList();
if (overlapping.Any())
{
foreach (var overlap in overlapping)
overlap.OverlappingField = true;
}
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
if (field is ArrayFieldReference arrayField)
{
_hasArraySearches = true;
existingSameSearchField = _searchFields.Where(x =>
x.Field is ArrayFieldReference arrayFieldRef
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
&& arrayFieldRef.Depth == arrayField.Depth
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
}
else if (field is PropertyFieldReference propField)
{
existingSameSearchField = _searchFields.Where(x =>
x.Field is PropertyFieldReference propFieldRef
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
&& propFieldRef.Depth == propField.Depth
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
}
foreach(var sameSearchField in existingSameSearchField)
{
if (sameSearchField.SkipReading == true
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
{
sameSearchField.SkipReading = false;
}
if (evaluator.ForceIfFound)
{
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
throw new Exception("Invalid config");
//sameSearchField.ForceEvaluator = evaluator;
}
}
_searchFields.Add(new MessageEvalutorFieldReference(field)
{
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
OverlappingField = overlapping.Any()
});
if (field.Depth > _maxSearchDepth)
_maxSearchDepth = field.Depth;
}
}
_initialized = true;
}
/// <inheritdoc />
public virtual string? GetTopicFilter(object deserializedObject)
{
if (_mapping == null)
return null;
// Cache the found type for future
var currentType = deserializedObject.GetType();
if (_baseTypeMapping != null)
{
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
return typeMapping(deserializedObject);
}
var mappedBase = false;
while (currentType != null)
{
if (_mapping.TryGetValue(currentType, out var mapping))
{
if (mappedBase)
{
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
}
return mapping(deserializedObject);
}
mappedBase = true;
currentType = currentType.BaseType;
}
return null;
}
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
InitializeConverter();
int? arrayIndex = null;
_searchResult.Clear();
var reader = new Utf8JsonReader(data);
while (reader.Read())
{
if ((reader.TokenType == JsonTokenType.StartArray
|| reader.TokenType == JsonTokenType.StartObject)
&& reader.CurrentDepth == _maxSearchDepth)
{
// There is no field we need to search for on a depth deeper than this, skip
reader.Skip();
continue;
}
if (reader.TokenType == JsonTokenType.StartArray)
arrayIndex = -1;
else if (reader.TokenType == JsonTokenType.EndArray)
arrayIndex = null;
else if (arrayIndex != null)
arrayIndex++;
if (reader.TokenType == JsonTokenType.PropertyName
|| arrayIndex != null && _hasArraySearches)
{
bool written = false;
string? value = null;
byte[]? propName = null;
foreach (var field in _searchFields!)
{
if (field.Field.Depth != reader.CurrentDepth)
continue;
bool readArrayValues = false;
if (field.Field is PropertyFieldReference propFieldRef)
{
if (propName == null)
{
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
continue;
propName = propFieldRef.PropertyName;
readArrayValues = propFieldRef.ArrayValues;
reader.Read();
}
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
{
continue;
}
}
else if (field.Field is ArrayFieldReference arrayFieldRef)
{
if (propName != null)
continue;
if (reader.TokenType == JsonTokenType.PropertyName)
continue;
if (arrayFieldRef.ArrayIndex != arrayIndex)
continue;
}
if (!field.SkipReading)
{
if (value == null)
{
if (readArrayValues)
{
if (reader.TokenType != JsonTokenType.StartArray)
// error
return null;
var sb = new StringBuilder();
reader.Read();// Read start array
bool first = true;
while(reader.TokenType != JsonTokenType.EndArray)
{
if (!first)
sb.Append(",");
first = false;
sb.Append(reader.GetString());
reader.Read();
}
value = first ? null : sb.ToString();
}
else
{
switch (reader.TokenType)
{
case JsonTokenType.Number:
value = reader.GetDecimal().ToString();
break;
case JsonTokenType.String:
value = reader.GetString()!;
break;
case JsonTokenType.True:
case JsonTokenType.False:
value = reader.GetBoolean().ToString()!;
break;
case JsonTokenType.Null:
value = null;
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
value = null;
break;
default:
continue;
}
}
}
if (field.Field.Constraint != null
&& !field.Field.Constraint(value))
{
continue;
}
}
_searchResult.Write(field.Field, value);
if (field.ForceEvaluator != null)
{
if (field.ForceEvaluator.StaticIdentifier != null)
return field.ForceEvaluator.StaticIdentifier;
// Force the immediate return upon encountering this field
return field.ForceEvaluator.GetMessageType(_searchResult);
}
written = true;
if (!field.OverlappingField)
break;
}
if (!written)
continue;
if (_topEvaluator!.Satisfied(_searchResult))
return _topEvaluator.GetMessageType(_searchResult);
if (_searchFields.Count == _searchResult.Count)
break;
}
}
foreach (var evaluator in TypeEvaluators)
{
if (evaluator.Satisfied(_searchResult))
return evaluator.GetMessageType(_searchResult);
}
return null;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#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.
return JsonSerializer.Deserialize(data, type, Options)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
}
}

View File

@ -0,0 +1,63 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
using System.Net.WebSockets;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
/// </summary>
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
{
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
var reader = new Utf8JsonReader(data);
var jsonDocument = JsonDocument.ParseValue(ref reader);
return GetTypeIdentifier(jsonDocument);
}
/// <summary>
/// Get the message identifier for this document
/// </summary>
protected abstract string? GetTypeIdentifier(JsonDocument document);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
public virtual string? GetTopicFilter(object deserializedObject) => null;
/// <inheritdoc />
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#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.
return JsonSerializer.Deserialize(data, type, Options)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
/// <summary>
/// Get the string value for a path, or an emtpy string if not found
/// </summary>
protected string StringOrEmpty(JsonDocument document, string path)
{
if (!document.RootElement.TryGetProperty(path, out var element))
return string.Empty;
if (element.ValueKind == JsonValueKind.String)
return element.GetString() ?? string.Empty;
else if (element.ValueKind == JsonValueKind.Number)
return element.GetDecimal().ToString();
return string.Empty;
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class NullableEnumConverterFactory : JsonConverterFactory
{
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
{
_jsonTypeInfoResolver = jsonTypeInfoResolver;
}
public override bool CanConvert(Type typeToConvert)
{
var b = Nullable.GetUnderlyingType(typeToConvert);
if (b == null)
return false;
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
if (typeInfo == null)
return false;
return typeInfo.Converter is INullableConverterFactory;
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
return nullConverterFactory.CreateNullableConverter();
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Read string or number as string
/// </summary>
public class NumberStringConverter : JsonConverter<string?>
{
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt64(out var value))
return value.ToString();
return reader.GetDecimal().ToString();
}
try
{
return reader.GetString();
}
catch (Exception)
{
return null;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Diagnostics.CodeAnalysis;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for values which contain a nested json value
/// </summary>
public class ObjectStringConverter<T> : JsonConverter<T>
{
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value is null)
writer.WriteStringValue("");
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Replace a value on a string property
/// </summary>
public abstract class ReplaceConverter : JsonConverter<string>
{
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{
_replacementSets = replaceSets.Select(x =>
{
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
if (split.Length != 2)
throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute to mark a model as json serializable. Used for AOT compilation.
/// </summary>
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
public class SerializationModelAttribute : Attribute
{
/// <summary>
/// ctor
/// </summary>
public SerializationModelAttribute() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="type"></param>
public SerializationModelAttribute(Type type) { }
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Serializer options
/// </summary>
public static class SerializerOptions
{
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
/// <summary>
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
/// </summary>
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
{
if (!_cache.TryGetValue(typeResolver, out var options))
{
options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters =
{
new DateTimeConverter(),
new BoolConverter(),
new DecimalConverter(),
new IntConverter(),
new LongConverter(),
new NullableEnumConverterFactory(typeResolver)
},
TypeInfoResolver = typeResolver,
};
foreach (var converter in additionalConverters)
options.Converters.Add(converter);
options.TypeInfoResolver = typeResolver;
_cache.TryAdd(typeResolver, options);
}
return options;
}
}
}

View File

@ -0,0 +1,58 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
var result = new T();
result.QuantityInBaseAsset = baseQuantity;
result.QuantityInQuoteAsset = quoteQuantity;
result.QuantityInContracts = contractQuantity;
return result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
if (value.QuantityInBaseAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
if (value.QuantityInQuoteAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
if (value.QuantityInContracts == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInContracts.Value);
writer.WriteEndArray();
}
}
}

View File

@ -0,0 +1,44 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
{
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
reader.Read();
var baseAsset = reader.GetString()!;
reader.Read();
var quoteAsset = reader.GetString()!;
reader.Read();
var timeStr = reader.GetString()!;
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
}
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteStringValue(value.TradingMode.ToString());
writer.WriteStringValue(value.BaseAsset);
writer.WriteStringValue(value.QuoteAsset);
writer.WriteStringValue(value.DeliverTime?.ToString());
writer.WriteEndArray();
}
}
}

View File

@ -0,0 +1,373 @@
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;
}
}
}

View File

@ -0,0 +1,27 @@
using CryptoExchange.Net.Interfaces;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <inheritdoc />
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
{
private readonly JsonSerializerOptions _options;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
{
_options = options;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
#endif
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
}
}

View File

@ -1,36 +0,0 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// converter for milliseconds to datetime
/// </summary>
public class TimestampConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
var t = long.Parse(reader.Value.ToString());
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if(value == null)
writer.WriteValue((DateTime?)null);
else
writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
}

View File

@ -1,35 +0,0 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for nanoseconds to datetime
/// </summary>
public class TimestampNanoSecondsConverter : JsonConverter
{
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
var nanoSeconds = long.Parse(reader.Value.ToString());
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerNanosecond));
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).Ticks / ticksPerNanosecond));
}
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for seconds to datetime
/// </summary>
public class TimestampSecondsConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
if (reader.Value is double d)
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d);
var t = double.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture);
// Set ticks instead of seconds or milliseconds, because AddSeconds/AddMilliseconds rounds to nearest millisecond
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)(t * TimeSpan.TicksPerSecond));
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
writer.WriteValue((DateTime?)null);
else
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalSeconds));
}
}
}

View File

@ -1,44 +0,0 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// converter for datetime string (yyyymmdd) to datetime
/// </summary>
public class TimestampStringConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
var value = reader.Value.ToString();
if (value.Length == 8)
return new DateTime(int.Parse(value.Substring(0, 4)), int.Parse(value.Substring(4, 2)), int.Parse(value.Substring(6, 2)), 0, 0, 0, DateTimeKind.Utc);
else if(value.Length == 6)
return new DateTime(int.Parse(value.Substring(0, 2)), int.Parse(value.Substring(2, 2)), int.Parse(value.Substring(4, 2)), 0, 0, 0, DateTimeKind.Utc);
throw new Exception("Unknown datetime value: " + value);
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
writer.WriteValue((DateTime?)null);
else
{
var dateTimeValue = (DateTime)value;
writer.WriteValue(int.Parse($"{dateTimeValue.Year}{dateTimeValue.Month}{dateTimeValue.Day}"));
}
}
}
}

View File

@ -1,38 +0,0 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for utc datetime
/// </summary>
public class UTCDateTimeConverter: JsonConverter
{
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue(JsonConvert.SerializeObject(value));
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
DateTime value;
if (reader.Value is string s)
value = (DateTime)JsonConvert.DeserializeObject(s)!;
else
value = (DateTime) reader.Value;
return DateTime.SpecifyKind(value, DateTimeKind.Utc);
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
}
}
}

View File

@ -1,25 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency exchange API's</Description>
<PackageVersion>4.2.8</PackageVersion>
<AssemblyVersion>4.2.8</AssemblyVersion>
<FileVersion>4.2.8</FileVersion>
<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.2</PackageVersion>
<AssemblyVersion>10.0.2</AssemblyVersion>
<FileVersion>10.0.2</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>4.2.8 - Fixed deadlock in socket receive, Fixed issue in reconnection handling when the client is disconnected again during resubscribing, Added some additional checking of socket state to prevent sending/expecting data when socket is not connected</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
<LangVersion>latest</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="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>
@ -27,25 +37,27 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="System.Text.Json" Version="10.0.1" />
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
</ItemGroup>
<ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using System;
namespace CryptoExchange.Net.Exceptions
{
/// <summary>
/// Exception during deserialization
/// </summary>
public class CeDeserializationException : Exception
{
/// <summary>
/// ctor
/// </summary>
public CeDeserializationException(string message) : base(message)
{
}
/// <summary>
/// ctor
/// </summary>
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -1,5 +1,13 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net
{
@ -8,6 +16,30 @@ namespace CryptoExchange.Net
/// </summary>
public static class ExchangeHelpers
{
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
private const string _allowedRandomHexChars = "0123456789ABCDEF";
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
{
{ 1, "F" },
{ 2, "G" },
{ 3, "H" },
{ 4, "J" },
{ 5, "K" },
{ 6, "M" },
{ 7, "N" },
{ 8, "Q" },
{ 9, "U" },
{ 10, "V" },
{ 11, "X" },
{ 12, "Z" },
};
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// </summary>
private static int _lastId;
/// <summary>
/// Clamp a value between a min and max
/// </summary>
@ -43,7 +75,14 @@ namespace CryptoExchange.Net
var offset = value % step.Value;
if(roundingType == RoundingType.Down)
{
value -= offset;
}
else if(roundingType == RoundingType.Up)
{
if (offset != 0)
value += (step.Value - offset);
}
else
{
if (offset < step / 2)
@ -51,8 +90,6 @@ namespace CryptoExchange.Net
else value += (step.Value - offset);
}
value = RoundDown(value, 8);
return value.Normalize();
}
@ -75,6 +112,34 @@ namespace CryptoExchange.Net
return RoundToSignificantDigits(value, precision.Value, roundingType);
}
/// <summary>
/// Apply the provided rules to the value
/// </summary>
/// <param name="value">Value to be adjusted</param>
/// <param name="decimals">Max decimal places</param>
/// <param name="valueStep">The value step for increase/decrease value</param>
/// <returns></returns>
public static decimal ApplyRules(
decimal value,
int? decimals = null,
decimal? valueStep = null)
{
if (valueStep.HasValue)
{
var offset = value % valueStep.Value;
if (offset != 0)
{
if (offset < valueStep.Value / 2)
value -= offset;
else value += (valueStep.Value - offset);
}
}
if (decimals.HasValue)
value = Math.Round(value, decimals.Value);
return value;
}
/// <summary>
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
/// </summary>
@ -96,17 +161,23 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Rounds a value down to
/// Rounds a value down
/// </summary>
/// <param name="i"></param>
/// <param name="decimalPlaces"></param>
/// <returns></returns>
public static decimal RoundDown(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power;
}
/// <summary>
/// Rounds a value up
/// </summary>
public static decimal RoundUp(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Ceiling(i * power) / power;
}
/// <summary>
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
/// </summary>
@ -116,5 +187,300 @@ namespace CryptoExchange.Net
{
return value / 1.000000000000000000000000000000000m;
}
/// <summary>
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
/// </summary>
/// <returns></returns>
public static int NextId() => Interlocked.Increment(ref _lastId);
/// <summary>
/// Return the last unique id that was generated
/// </summary>
/// <returns></returns>
public static int LastId() => _lastId;
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="length">Length of the random string</param>
/// <returns></returns>
public static string RandomString(int length)
{
var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else
var random = new Random();
for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
#endif
return new string(randomChars);
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="length">Length of the random string</param>
/// <returns></returns>
public static string RandomHexString(int length)
{
#if NET9_0_OR_GREATER
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
#else
var randomChars = new char[length * 2];
var random = new Random();
for (int i = 0; i < length * 2; i++)
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
return "0x" + new string(randomChars);
#endif
}
/// <summary>
/// Generate a long value
/// </summary>
/// <param name="maxLength">Max character length</param>
/// <returns></returns>
public static long RandomLong(int maxLength)
{
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
#else
var random = new Random();
var value = random.Next(0, int.MaxValue);
#endif
var val = value.ToString();
if (val.Length > maxLength)
return int.Parse(val.Substring(0, maxLength));
else
return value;
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="source">The initial string</param>
/// <param name="totalLength">Total length of the resulting string</param>
/// <returns></returns>
public static string AppendRandomString(string source, int totalLength)
{
if (totalLength < source.Length)
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
if (totalLength == source.Length)
return source;
return source + RandomString(totalLength - source.Length);
}
/// <summary>
/// Get the month representation for futures symbol based on the delivery month
/// </summary>
/// <param name="time">Delivery time</param>
/// <returns></returns>
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
/// <summary>
/// Execute multiple requests to retrieve multiple pages of the result set
/// </summary>
/// <typeparam name="T">Type of the client</typeparam>
/// <typeparam name="U">Type of the request</typeparam>
/// <param name="paginatedFunc">The func to execute with each request</param>
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <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)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
INextPageToken? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch;
if (!batch || ct.IsCancellationRequested)
break;
result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken;
if (nextPageToken == null)
break;
}
}
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
/// <param name="symbol">The symbol as retrieved from the exchange</param>
/// <param name="quantity">Quantity to trade</param>
/// <param name="price">Price to trade at</param>
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
{
adjustedPrice = price;
adjustedQuantity = quantity;
var minNotionalAdjust = false;
if (price != null)
{
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
minNotionalAdjust = true;
}
}
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
}
/// <summary>
/// Queue updates received from a websocket subscriptions and process them async
/// </summary>
/// <typeparam name="T">The queued update type</typeparam>
/// <param name="subscribeCall">The subscribe call</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="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>(
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<T>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
{
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
if (!result)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
}
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
result.Data.SubscriptionStatusChanged += (upd) =>
{
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
_ = processor.StopAsync(true);
};
return result;
}
/// <summary>
/// Queue updates and process them async
/// </summary>
/// <typeparam name="T">The queued update type</typeparam>
/// <param name="subscribeCall">The subscribe call</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="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="ct">Cancellation token to stop the processing</param>
public static async Task ProcessQueuedAsync<T>(
Func<Action<T>, Task> subscribeCall,
Func<T, Task> asyncHandler,
CancellationToken ct,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
{
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
ct.Register(async () =>
{
await processor.StopAsync().ConfigureAwait(false);
});
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
}
/// <summary>
/// Queue updates received from a websocket subscriptions and process them async
/// </summary>
/// <typeparam name="TEventType">The type of the queued item</typeparam>
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></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="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<TEventType, TOutputType>(
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
Func<DataEvent<TOutputType>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null
)
{
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
return asyncHandler.Invoke(mapper.Invoke(update));
}, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(processor).ConfigureAwait(false);
if (!result)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
}
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
result.Data.SubscriptionStatusChanged += (upd) =>
{
if (upd == SubscriptionStatus.Closed)
_ = processor.StopAsync(true);
};
return result;
}
/// <summary>
/// Parse a decimal value from a string
/// </summary>
public static decimal? ParseDecimal(string? value)
{
// Value is null or empty is the most common case to return null so check before trying to parse
if (string.IsNullOrEmpty(value))
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
return decValue;
// Check for values which should be parsed to null
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Infinity value should be parsed to min/max value
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
// assume overflow
if (overflowValue < 0)
return decimal.MinValue;
else
return decimal.MaxValue;
}
// Unknown decimal format, return null
return null;
}
}
}

View File

@ -1,21 +0,0 @@
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common balance
/// </summary>
public interface ICommonBalance
{
/// <summary>
/// The asset name
/// </summary>
public string CommonAsset { get; }
/// <summary>
/// Amount available
/// </summary>
public decimal CommonAvailable { get; }
/// <summary>
/// Total amount
/// </summary>
public decimal CommonTotal { get; }
}
}

View File

@ -1,35 +0,0 @@
using System;
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common trade
/// </summary>
public interface ICommonTrade
{
/// <summary>
/// Id of the trade
/// </summary>
public string CommonId { get; }
/// <summary>
/// Price of the trade
/// </summary>
public decimal CommonPrice { get; }
/// <summary>
/// Quantity of the trade
/// </summary>
public decimal CommonQuantity { get; }
/// <summary>
/// Fee paid for the trade
/// </summary>
public decimal CommonFee { get; }
/// <summary>
/// The asset fee was paid in
/// </summary>
public string? CommonFeeAsset { get; }
/// <summary>
/// Trade time
/// </summary>
DateTime CommonTradeTime { get; }
}
}

View File

@ -1,177 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Shared interface for exchange wrappers based on the CryptoExchange.Net package
/// </summary>
public interface IExchangeClient
{
/// <summary>
/// Should be triggered on order placing
/// </summary>
event Action<ICommonOrderId> OnOrderPlaced;
/// <summary>
/// Should be triggered on order cancelling
/// </summary>
event Action<ICommonOrderId> OnOrderCanceled;
/// <summary>
/// Get the symbol name based on a base and quote asset
/// </summary>
/// <param name="baseAsset"></param>
/// <param name="quoteAsset"></param>
/// <returns></returns>
string GetSymbolName(string baseAsset, string quoteAsset);
/// <summary>
/// Get a list of symbols for the exchange
/// </summary>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonSymbol>>> GetSymbolsAsync();
/// <summary>
/// Get a list of tickers for the exchange
/// </summary>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonTicker>>> GetTickersAsync();
/// <summary>
/// Get a ticker for the exchange
/// </summary>
/// <param name="symbol">The symbol to get klines for</param>
/// <returns></returns>
Task<WebCallResult<ICommonTicker>> GetTickerAsync(string symbol);
/// <summary>
/// Get a list of candles for a given symbol on the exchange
/// </summary>
/// <param name="symbol">The symbol to retrieve the candles for</param>
/// <param name="timespan">The timespan to retrieve the candles for. The supported value are dependent on the exchange</param>
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
/// <param name="limit">[Optional] Max number of results</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonKline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
/// <summary>
/// Get the order book for a symbol
/// </summary>
/// <param name="symbol">The symbol to get the book for</param>
/// <returns></returns>
Task<WebCallResult<ICommonOrderBook>> GetOrderBookAsync(string symbol);
/// <summary>
/// The recent trades for a symbol
/// </summary>
/// <param name="symbol">The symbol to get the trades for</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonRecentTrade>>> GetRecentTradesAsync(string symbol);
/// <summary>
/// Place an order
/// </summary>
/// <param name="symbol">The symbol the order is for</param>
/// <param name="side">The side of the order</param>
/// <param name="type">The type of the order</param>
/// <param name="quantity">The quantity of the order</param>
/// <param name="price">The price of the order, only for limit orders</param>
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<ICommonOrderId>> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal quantity, decimal? price = null, string? accountId = null);
/// <summary>
/// Get an order by id
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <returns></returns>
Task<WebCallResult<ICommonOrder>> GetOrderAsync(string orderId, string? symbol = null);
/// <summary>
/// Get trades for an order by id
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonTrade>>> GetTradesAsync(string orderId, string? symbol = null);
/// <summary>
/// Get a list of open orders
/// </summary>
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetOpenOrdersAsync(string? symbol = null);
/// <summary>
/// Get a list of closed orders
/// </summary>
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetClosedOrdersAsync(string? symbol = null);
/// <summary>
/// Cancel an order by id
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <returns></returns>
Task<WebCallResult<ICommonOrderId>> CancelOrderAsync(string orderId, string? symbol = null);
/// <summary>
/// Get balances
/// </summary>
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonBalance>>> GetBalancesAsync(string? accountId = null);
/// <summary>
/// Common order id
/// </summary>
public enum OrderType
{
/// <summary>
/// Limit type
/// </summary>
Limit,
/// <summary>
/// Market type
/// </summary>
Market,
/// <summary>
/// Other order type
/// </summary>
Other
}
/// <summary>
/// Common order side
/// </summary>
public enum OrderSide
{
/// <summary>
/// Buy order
/// </summary>
Buy,
/// <summary>
/// Sell order
/// </summary>
Sell
}
/// <summary>
/// Common order status
/// </summary>
public enum OrderStatus
{
/// <summary>
/// placed and not fully filled order
/// </summary>
Active,
/// <summary>
/// cancelled order
/// </summary>
Canceled,
/// <summary>
/// filled order
/// </summary>
Filled
}
}
}

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