1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-07 07:56:12 +00:00

Compare commits

...

89 Commits

Author SHA1 Message Date
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
150 changed files with 9454 additions and 8259 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: 6.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

@ -11,26 +11,12 @@ 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("")
var client = new TestBaseClient(new BaseRestClientOptions()
{
LogWriters = new List<ILogger> { logger }
});
@ -65,16 +51,18 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(null, LogLevel.Error, true)]
[TestCase(null, LogLevel.Warning, true)]
[TestCase(null, LogLevel.Information, true)]
[TestCase(null, LogLevel.Debug, true)]
[TestCase(null, LogLevel.Debug, false)]
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
{
// arrange
var logger = new TestStringLogger();
var client = new TestBaseClient(new RestClientOptions("")
var options = new BaseRestClientOptions()
{
LogWriters = new List<ILogger> { logger },
LogLevel = verbosity
});
LogWriters = new List<ILogger> { logger }
};
if (verbosity != null)
options.LogLevel = verbosity.Value;
var client = new TestBaseClient(options);
// act
client.Log(testVerbosity, "Test");
@ -110,17 +98,17 @@ namespace CryptoExchange.Net.UnitTests
Assert.IsTrue(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.AreEqual(expected, result);
}
}
}

View File

@ -0,0 +1,176 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
internal class CallResultTests
{
[Test]
public void TestBasicErrorCallResult()
{
var result = new CallResult(new ServerError("TestError"));
Assert.AreEqual(result.Error.Message, "TestError");
Assert.IsFalse(result);
Assert.IsFalse(result.Success);
}
[Test]
public void TestBasicSuccessCallResult()
{
var result = new CallResult(null);
Assert.IsNull(result.Error);
Assert.IsTrue(result);
Assert.IsTrue(result.Success);
}
[Test]
public void TestCallResultError()
{
var result = new CallResult<object>(new ServerError("TestError"));
Assert.AreEqual(result.Error.Message, "TestError");
Assert.IsNull(result.Data);
Assert.IsFalse(result);
Assert.IsFalse(result.Success);
}
[Test]
public void TestCallResultSuccess()
{
var result = new CallResult<object>(new object());
Assert.IsNull(result.Error);
Assert.IsNotNull(result.Data);
Assert.IsTrue(result);
Assert.IsTrue(result.Success);
}
[Test]
public void TestCallResultSuccessAs()
{
var result = new CallResult<TestObjectResult>(new TestObjectResult());
var asResult = result.As<TestObject2>(result.Data.InnerData);
Assert.IsNull(asResult.Error);
Assert.IsNotNull(asResult.Data);
Assert.IsTrue(asResult.Data is TestObject2);
Assert.IsTrue(asResult);
Assert.IsTrue(asResult.Success);
}
[Test]
public void TestCallResultErrorAs()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.As<TestObject2>(default);
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError");
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
public void TestCallResultErrorAsError()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError2");
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultErrorAsError()
{
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError2");
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultSuccessAsError()
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(),
TimeSpan.FromSeconds(1),
"{}",
"https://test.com/api",
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
new TestObjectResult(),
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
Assert.IsNotNull(asResult.Error);
Assert.AreEqual(asResult.Error.Message, "TestError2");
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
Assert.IsNull(asResult.Data);
Assert.IsFalse(asResult);
Assert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultSuccessAsSuccess()
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(),
TimeSpan.FromSeconds(1),
"{}",
"https://test.com/api",
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
new TestObjectResult(),
null);
var asResult = result.As<TestObject2>(result.Data.InnerData);
Assert.IsNull(asResult.Error);
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
Assert.IsNotNull(asResult.Data);
Assert.IsTrue(asResult);
Assert.IsTrue(asResult.Success);
}
}
public class TestObjectResult
{
public TestObject2 InnerData;
public TestObjectResult()
{
InnerData = new TestObject2();
}
}
public class TestObject2
{
}
}

View File

@ -0,0 +1,194 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ConverterTests
{
[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("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.AreEqual(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 = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
Assert.AreEqual(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 = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
Assert.AreEqual(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.AreEqual(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.AreEqual(output, 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.AreEqual(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.AreEqual(output, 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.AreEqual(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.AreEqual(output, 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.AreEqual(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.AreEqual(output, 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
Assert.AreEqual(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.AreEqual(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.AreEqual(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 = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
Assert.AreEqual(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 = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
Assert.AreEqual(output.Value, expected);
}
}
public class TimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
public DateTime? Time { get; set; }
}
public class EnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableEnumObject
{
public TestEnum Value { get; set; }
}
[JsonConverter(typeof(EnumConverter))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
}

View File

@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"></packagereference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></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="NUnit" Version="3.13.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,295 @@
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 System.Linq;
using System.Text;
using System.Threading.Tasks;
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 RestApiClientOptions() { 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.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
}
[Test]
public void TestApiOptionsAreSet()
{
// arrange, act
var options = new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
BaseAddress = "http://test1.com"
},
Api2Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("789", "101"),
BaseAddress = "http://test2.com"
}
};
// assert
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
Assert.AreEqual(options.Api2Options.BaseAddress, "http://test2.com");
}
[Test]
public void TestNotOverridenApiOptionsAreStillDefault()
{
// arrange, act
var options = new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
}
};
// assert
Assert.AreEqual(options.Api1Options.RateLimitingBehaviour, RateLimitingBehaviour.Wait);
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
Assert.AreEqual(options.Api2Options.BaseAddress, "https://api2.test.com/");
}
[Test]
public void TestSettingDefaultBaseOptionsAreRespected()
{
// arrange
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
LogLevel = LogLevel.Trace
};
// act
var options = new TestClientOptions();
// assert
Assert.AreEqual(options.LogLevel, LogLevel.Trace);
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
}
[Test]
public void TestSettingDefaultApiOptionsAreRespected()
{
// arrange
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
LogLevel = LogLevel.Trace,
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("456", "789")
}
};
// act
var options = new TestClientOptions();
// assert
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "456");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "789");
}
[Test]
public void TestSettingDefaultApiOptionsWithSomeOverriddenAreRespected()
{
// arrange
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new ApiCredentials("123", "456"),
LogLevel = LogLevel.Trace,
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("456", "789")
},
Api2Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
// act
var options = new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("333", "444")
}
};
// assert
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "333");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "444");
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "111");
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "222");
}
[Test]
public void TestClientUsesCorrectOptions()
{
var client = new TestRestClient(new TestClientOptions()
{
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
});
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
}
[Test]
public void TestClientUsesCorrectOptionsWithDefault()
{
TestClientOptions.Default = new TestClientOptions()
{
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
var client = new TestRestClient();
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
}
[Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault()
{
TestClientOptions.Default = new TestClientOptions()
{
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
var client = new TestRestClient(new TestClientOptions
{
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("333", "444")
},
Api2Options = new RestApiClientOptions()
{
BaseAddress = "http://test.com"
}
});
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
}
}
public class TestClientOptions: BaseRestClientOptions
{
/// <summary>
/// Default options for the futures client
/// </summary>
public static TestClientOptions Default { get; set; } = new TestClientOptions();
/// <summary>
/// The default receive window for requests
/// </summary>
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
public RestApiClientOptions Api1Options
{
get => _api1Options;
set => _api1Options = new RestApiClientOptions(_api1Options, value);
}
private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/");
public RestApiClientOptions Api2Options
{
get => _api2Options;
set => _api2Options = new RestApiClientOptions(_api2Options, value);
}
/// <summary>
/// ctor
/// </summary>
public TestClientOptions(): this(Default)
{
}
public TestClientOptions(TestClientOptions baseOn): base(baseOn)
{
if (baseOn == null)
return;
ReceiveWindow = baseOn.ReceiveWindow;
Api1Options = new RestApiClientOptions(baseOn.Api1Options, null);
Api2Options = new RestApiClientOptions(baseOn.Api2Options, null);
}
}
}

View File

@ -8,10 +8,11 @@ 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 CryptoExchange.Net.Logging;
using System.Threading;
namespace CryptoExchange.Net.UnitTests
{
@ -50,14 +51,14 @@ namespace CryptoExchange.Net.UnitTests
}
[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.Request<TestObject>();
// assert
Assert.IsFalse(result.Success);
@ -65,14 +66,14 @@ namespace CryptoExchange.Net.UnitTests
}
[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.Request<TestObject>();
// assert
Assert.IsFalse(result.Success);
@ -83,14 +84,14 @@ namespace CryptoExchange.Net.UnitTests
}
[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.Request<TestObject>();
// assert
Assert.IsFalse(result.Success);
@ -105,20 +106,23 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
// act
var client = new TestRestClient(new RestClientOptions("")
var client = new TestRestClient(new TestClientOptions()
{
BaseAddress = "http://test.address.com",
RateLimiters = new List<IRateLimiter>{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))},
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
Api1Options = new RestApiClientOptions
{
BaseAddress = "http://test.address.com",
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
RateLimitingBehaviour = RateLimitingBehaviour.Fail
},
RequestTimeout = TimeSpan.FromMinutes(1)
});
// 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.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
@ -132,12 +136,15 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
// act
var client = new TestRestClient(new RestClientOptions("")
var client = new TestRestClient(new TestClientOptions()
{
BaseAddress = "http://test.address.com",
Api1Options = new RestApiClientOptions
{
BaseAddress = "http://test.address.com"
}
});
client.SetParameterPosition(new HttpMethod(method), pos);
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
client.SetResponse("{}", out var request);
@ -161,84 +168,199 @@ namespace CryptoExchange.Net.UnitTests
Assert.IsTrue(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 log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
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 _);
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
}
// 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);
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result2.Data == 0);
}
[TestCase]
public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests()
[TestCase("/sapi/test1", true)]
[TestCase("/sapi/test2", true)]
[TestCase("/api/test1", false)]
[TestCase("sapi/test1", false)]
[TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{
// arrange
var client = new TestRestClient(new RestClientOptions("")
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
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 _);
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected);
}
}
[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)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
// 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.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
}
[TestCase]
public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey()
[TestCase(1, 0.1)]
[TestCase(2, 0.1)]
[TestCase(5, 1)]
[TestCase(1, 2)]
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
{
// arrange
var client = new TestRestClient(new RestClientOptions("")
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
for (var i = 0; i < requests + 1; i++)
{
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 result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
}
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result2.Data == 0);
}
// 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();
[TestCase("/", false)]
[TestCase("/sapi/test", true)]
[TestCase("/sapi/test/123", false)]
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
// assert
Assert.IsTrue(result1.Success);
Assert.IsTrue(result2.Success);
Assert.IsTrue(result3.Success);
Assert.IsTrue(sw.ElapsedMilliseconds > 900 && sw.ElapsedMilliseconds < 1900, $"Actual: {sw.ElapsedMilliseconds}");
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected);
}
}
[TestCase("/", false)]
[TestCase("/sapi/test", true)]
[TestCase("/sapi/test2", true)]
[TestCase("/sapi/test23", false)]
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected);
}
}
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
[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 log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
[TestCase("/sapi/test", true, true, true, false)]
[TestCase("/sapi/test", false, true, true, false)]
[TestCase("/sapi/test", false, true, false, true)]
[TestCase("/sapi/test", true, true, false, true)]
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
{
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
}
}
}

View File

@ -17,16 +17,19 @@ namespace CryptoExchange.Net.UnitTests
{
//arrange
//act
var client = new TestSocketClient(new SocketClientOptions("")
var client = new TestSocketClient(new TestOptions()
{
BaseAddress = "http://test.address.com",
SubOptions = new RestApiClientOptions
{
BaseAddress = "http://test.address.com"
},
ReconnectInterval = TimeSpan.FromSeconds(6)
});
//assert
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
Assert.IsTrue(client.ReconnectInterval.TotalSeconds == 6);
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6);
}
[TestCase(true)]
@ -39,7 +42,7 @@ namespace CryptoExchange.Net.UnitTests
socket.CanConnect = canConnect;
//act
var connectResult = client.ConnectSocketSub(new SocketConnection(client, socket));
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket, null));
//assert
Assert.IsTrue(connectResult.Success == canConnect);
@ -49,15 +52,15 @@ namespace CryptoExchange.Net.UnitTests
public void SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var client = new TestSocketClient(new TestOptions() { 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 sub = new SocketConnection(client, null, socket, null);
var rstEvent = new ManualResetEvent(false);
JToken result = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
{
result = messageEvent.JsonData;
rstEvent.Set();
@ -77,15 +80,15 @@ namespace CryptoExchange.Net.UnitTests
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
var client = new TestSocketClient(new TestOptions() { 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 sub = new SocketConnection(client, null, socket, null);
var rstEvent = new ManualResetEvent(false);
string original = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
@ -100,44 +103,18 @@ namespace CryptoExchange.Net.UnitTests
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
}
[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.InvokeClose();
rstEvent.WaitOne(1000);
// assert
Assert.IsTrue(reconnected);
}
[TestCase()]
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(client, socket);
var sub = new SocketConnection(client, null, socket, null);
client.ConnectSocketSub(sub);
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
var ups = new UpdateSubscription(sub, us);
sub.AddSubscription(us);
// act
client.UnsubscribeAsync(ups).Wait();
@ -150,13 +127,13 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var client = new TestSocketClient(new TestOptions() { 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);
var sub1 = new SocketConnection(client, null, socket1, null);
var sub2 = new SocketConnection(client, null, socket2, null);
client.ConnectSocketSub(sub1);
client.ConnectSocketSub(sub2);
@ -172,10 +149,10 @@ namespace CryptoExchange.Net.UnitTests
public void FailingToConnectSocket_Should_ReturnError()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.CanConnect = false;
var sub = new SocketConnection(client, socket);
var sub = new SocketConnection(client, null, socket, null);
// act
var connectResult = client.ConnectSocketSub(sub);

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
@ -12,22 +13,21 @@ namespace CryptoExchange.Net.UnitTests
[TestFixture]
public class SymbolOrderBookTests
{
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions("Test", true, false);
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
private class TestableSymbolOrderBook : SymbolOrderBook
{
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
public TestableSymbolOrderBook() : base("Test", "BTC/USD", 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();
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
@ -8,11 +9,11 @@ namespace CryptoExchange.Net.UnitTests
{
public class TestBaseClient: BaseClient
{
public TestBaseClient(): base("Test", new RestClientOptions("http://testurl.url"), null)
public TestBaseClient(): base("Test", new BaseClientOptions())
{
}
public TestBaseClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions)
{
}
@ -23,12 +24,7 @@ namespace CryptoExchange.Net.UnitTests
public CallResult<T> Deserialize<T>(string data)
{
return Deserialize<T>(data, false);
}
public string FillParameters(string path, params string[] values)
{
return FillPathParameter(path, values);
return Deserialize<T>(data, null, null);
}
}
@ -38,14 +34,11 @@ namespace CryptoExchange.Net.UnitTests
{
}
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
{
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);
bodyParameters = new SortedDictionary<string, object>();
uriParameters = new SortedDictionary<string, object>();
headers = new Dictionary<string, string>();
}
public override string Sign(string toSign)

View File

@ -15,28 +15,22 @@ using System.Collections.Generic;
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() : this(new TestClientOptions())
{
}
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
{
Api1 = new TestRestApi1Client(exchangeOptions);
Api2 = new TestRestApi2Client(exchangeOptions);
RequestFactory = new Mock<IRequestFactory>().Object;
}
public TestRestClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
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)));
}
public void SetResponse(string responseData, out IRequest requestObj)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
@ -57,10 +51,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
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) =>
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((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);
@ -73,10 +67,12 @@ 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 Dictionary<string, IEnumerable<string>>());
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>()))
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
}
@ -99,19 +95,75 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
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) => request.Setup(a => a.Uri).Returns(new Uri(uri)))
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((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);
return await SendRequestAsync<T>(Api1, 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
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
}
}
public class TestRestApi1Client : RestApiClient
{
public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options)
{
}
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
{
ParameterPositions[method] = position;
}
public override TimeSpan GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestRestApi2Client : RestApiClient
{
public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options)
{
}
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();
}
}
@ -120,12 +172,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{
}
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
{
uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
headers = new Dictionary<string, string>();
}
}
public class ParseErrorTestRestClient: TestRestClient
{
public ParseErrorTestRestClient() { }
public ParseErrorTestRestClient(RestClientOptions exchangeOptions) : base(exchangeOptions) { }
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
protected override Error ParseErrorResponse(JToken error)
{

View File

@ -13,9 +13,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public bool Connected { get; set; }
public event Action OnClose;
#pragma warning disable 0067
public event Action OnReconnected;
public event Action OnReconnecting;
#pragma warning restore 0067
public event Action<string> OnMessage;
public event Action<Exception> OnError;
public event Action OnOpen;
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
public int Id { get; }
public bool ShouldReconnect { get; set; }
@ -38,6 +44,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public double IncomingKbps => throw new NotImplementedException();
public Uri Uri => new Uri("");
public TimeSpan KeepAliveInterval { get; set; }
public static int lastId = 0;
public static object lastIdLock = new object();
@ -89,6 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
Connected = false;
DisconnectTime = DateTime.UtcNow;
Reconnecting = true;
OnClose?.Invoke();
}
@ -111,5 +122,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
OnError?.Invoke(error);
}
public Task ReconnectAsync() => Task.CompletedTask;
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
@ -9,22 +10,25 @@ using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestSocketClient: SocketClient
public class TestSocketClient: BaseSocketClient
{
public TestSocketClient() : this(new SocketClientOptions("http://testurl.url"))
public TestSubSocketClient SubClient { get; }
public TestSocketClient() : this(new TestOptions())
{
}
public TestSocketClient(SocketClientOptions exchangeOptions) : base("test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
{
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).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);
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
return (TestSocket)CreateSocket("https://localhost:123/");
}
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
@ -43,12 +47,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(JToken message, object request)
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(JToken message, string identifier)
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
{
return true;
}
@ -63,4 +67,21 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException();
}
}
public class TestOptions: BaseSocketClientOptions
{
public ApiClientOptions SubOptions { get; set; } = new ApiClientOptions();
}
public class TestSubSocketClient : SocketApiClient
{
public TestSubSocketClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
{
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
}
}

View File

@ -1,12 +1,18 @@

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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -21,10 +27,22 @@ 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
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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
EndGlobalSection

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,24 @@
using System;
namespace CryptoExchange.Net.Attributes
{
/// <summary>
/// Map a enum entry to string values
/// </summary>
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

@ -7,7 +7,7 @@ using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Api credentials info
/// Api credentials, used to sign requests accessing private endpoints
/// </summary>
public class ApiCredentials: IDisposable
{
@ -67,9 +67,10 @@ namespace CryptoExchange.Net.Authentication
/// Copy the credentials
/// </summary>
/// <returns></returns>
public ApiCredentials Copy()
public virtual ApiCredentials Copy()
{
if (PrivateKey == null)
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
else
return new ApiCredentials(PrivateKey!.Copy());

View File

@ -1,6 +1,11 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
namespace CryptoExchange.Net.Authentication
{
@ -14,45 +19,158 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
public ApiCredentials Credentials { get; }
/// <summary>
/// </summary>
protected byte[] _sBytes;
/// <summary>
/// ctor
/// </summary>
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
Credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
}
/// <summary>
/// Add authentication to the parameter list based on the provided credentials
/// Authenticate a request. Output parameters should include the providedParameters input
/// </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)
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="uri">The uri for the request</param>
/// <param name="method">The method of the request</param>
/// <param name="providedParameters">The request parameters</param>
/// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param>
public abstract void AuthenticateRequest(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
Dictionary<string, object> providedParameters,
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
out SortedDictionary<string, object> uriParameters,
out SortedDictionary<string, object> bodyParameters,
out Dictionary<string, string> headers
);
/// <summary>
/// SHA256 sign the data and return the bytes
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
protected static byte[] SignSHA256Bytes(string data)
{
return parameters;
using var encryptor = SHA256.Create();
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// Add authentication to the header dictionary based on the provided credentials
/// SHA256 sign the data and return the hash
/// </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)
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected static string SignSHA256(string data, SignOutputType? outputType = null)
{
return new Dictionary<string, string>();
using var encryptor = SHA256.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(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>
/// 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>
/// 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>
/// 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)
{
using var encryptor = new HMACSHA256(_sBytes);
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(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)
{
using var encryptor = new HMACSHA384(_sBytes);
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(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>
@ -76,16 +194,46 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Convert byte array to hex
/// 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)
{
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2"); /* hex format */
result += t.ToString("X2");
return result;
}
/// <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 static DateTime GetTimestamp(RestApiClient apiClient)
{
return DateTime.UtcNow.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 static string GetMillisecondTimestamp(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}
}
}

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,114 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net
{
/// <summary>
/// Base API for all API clients
/// </summary>
public abstract class BaseApiClient: IDisposable
{
private ApiCredentials? _apiCredentials;
private AuthenticationProvider? _authenticationProvider;
private bool _created;
private bool _disposing;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public AuthenticationProvider? AuthenticationProvider
{
get
{
if (!_created && !_disposing && _apiCredentials != null)
{
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
_created = true;
}
return _authenticationProvider;
}
}
/// <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 }
};
/// <summary>
/// Request body content type
/// </summary>
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// Whether or not we need to manually parse an error instead of relying on the http status code
/// </summary>
public bool manualParseError = false;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
public string requestBodyEmptyContent = "{}";
/// <summary>
/// The base address for this API client
/// </summary>
internal protected string BaseAddress { get; }
/// <summary>
/// Api client options
/// </summary>
internal ApiClientOptions Options { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="options">Client options</param>
/// <param name="apiOptions">Api client options</param>
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions)
{
Options = apiOptions;
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
BaseAddress = apiOptions.BaseAddress;
}
/// <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 void SetApiCredentials(ApiCredentials credentials)
{
_apiCredentials = credentials?.Copy();
_created = false;
_authenticationProvider = null;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_disposing = true;
_apiCredentials?.Dispose();
AuthenticationProvider?.Credentials?.Dispose();
}
}
}

View File

@ -0,0 +1,306 @@
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.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 name of the API the client is for
/// </summary>
internal string Name { get; }
/// <summary>
/// Api clients in this client
/// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary>
/// The log object
/// </summary>
protected internal Log log;
/// <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>
/// Provided client options
/// </summary>
public BaseClientOptions ClientOptions { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param>
protected BaseClient(string name, BaseClientOptions options)
{
log = new Log(name);
log.UpdateWriters(options.LogWriters);
log.Level = options.LogLevel;
options.OnLoggingChanged += HandleLogConfigChange;
ClientOptions = options;
Name = name;
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
}
/// <summary>
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
{
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Tries to parse the json data and return 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>(new DeserializeError(info, data));
}
try
{
return new CallResult<JToken>(JToken.Parse(data));
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<JToken>(new DeserializeError(info, data));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<JToken>(new DeserializeError(info, data));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<JToken>(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="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, JsonSerializer? serializer = null, int? requestId = null)
{
var tokenResult = ValidateJson(data);
if (!tokenResult)
{
log.Write(LogLevel.Error, tokenResult.Error!.Message);
return new CallResult<T>( tokenResult.Error);
}
return Deserialize<T>(tokenResult.Data, 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="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, JsonSerializer? serializer = null, int? requestId = null)
{
serializer ??= defaultSerializer;
try
{
return new CallResult<T>(obj.ToObject<T>(serializer)!);
}
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>(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>(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>(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)
{
serializer ??= defaultSerializer;
string? data = null;
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 (ClientOptions.OutputOriginalData || log.Level == LogLevel.Trace)
{
data = await reader.ReadToEndAsync().ConfigureAwait(false);
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(log.Level == LogLevel.Trace ? (": " + data) : "")}");
var result = Deserialize<T>(data, serializer, requestId);
if(ClientOptions.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);
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
}
catch (JsonReaderException jre)
{
if (data == null)
{
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 Trace 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>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
}
catch (JsonSerializationException jse)
{
if (data == null)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Trace LogLevel]";
}
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
}
catch (Exception ex)
{
if (data == null)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Trace LogLevel]";
}
var exceptionInfo = ex.ToLogString();
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
}
}
private static async Task<string> ReadStreamAsync(Stream stream)
{
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
/// <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 static int NextId()
{
lock (idLock)
{
lastId += 1;
return lastId;
}
}
/// <summary>
/// Handle a change in the client options log config
/// </summary>
private void HandleLogConfigChange()
{
log.UpdateWriters(ClientOptions.LogWriters);
log.Level = ClientOptions.LogLevel;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
log.Write(LogLevel.Debug, "Disposing client");
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
foreach (var client in ApiClients)
client.Dispose();
}
}
}

View File

@ -0,0 +1,496 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
/// <summary>
/// Base rest client
/// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary>
/// Client options
/// </summary>
public new BaseRestClientOptions ClientOptions { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param>
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
}
/// <inheritdoc />
public void SetApiCredentials(ApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Execute a request to the uri and returns if it was successful
/// </summary>
/// <param name="apiClient">The API client the request is for</param>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult> SendRequestAsync(RestApiClient apiClient,
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(apiClient, request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
return result.AsDataless();
}
/// <summary>
/// Execute a request to the uri and deserialize the response into the provided type parameter
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="apiClient">The API client the request is for</param>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false
) where T : class
{
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
return await GetResponseAsync<T>(apiClient, request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
}
/// <summary>
/// Prepares a request to be sent to the server
/// </summary>
/// <param name="apiClient">The API client the request is for</param>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <returns></returns>
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(RestApiClient apiClient,
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
var requestId = NextId();
if (signed)
{
var syncTask = apiClient.SyncTimeAsync();
var timeSyncInfo = apiClient.GetTimeSyncInfo();
if (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 syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
return syncTimeResult.As<IRequest>(default);
}
}
}
if (!ignoreRatelimit)
{
foreach (var limiter in apiClient.RateLimiters)
{
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
if (!limitResult.Success)
return new CallResult<IRequest>(limitResult.Error!);
}
}
if (signed && apiClient.AuthenticationProvider == null)
{
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new CallResult<IRequest>(new NoApiCredentialsError());
}
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
string? paramString = "";
if (paramsPosition == HttpMethodParameterPosition.InBody)
paramString = $" with request body '{request.Content}'";
var headers = request.GetHeaders();
if (headers.Any())
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
apiClient.TotalRequestsMade++;
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
return new CallResult<IRequest>(request);
}
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
/// <param name="apiClient">The client making the request</param>
/// <param name="request">The request object to execute</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
BaseApiClient apiClient,
IRequest request,
JsonSerializer? deserializer,
CancellationToken cancellationToken,
bool expectedEmptyResponse)
{
try
{
var sw = Stopwatch.StartNew();
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
if (apiClient.manualParseError)
{
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Close();
response.Close();
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}");
if (!expectedEmptyResponse)
{
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
var parseResult = ValidateJson(data);
if (!parseResult.Success)
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
// Let the library implementation see if it is an error response, and if so parse the error
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
// Not an error, so continue deserializing
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
}
else
{
if (!string.IsNullOrEmpty(data))
{
var parseResult = ValidateJson(data);
if (!parseResult.Success)
// Not empty, and not json
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
if (error != null)
// Error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
}
// Empty success response; okay
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
}
}
else
{
if (expectedEmptyResponse)
{
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
}
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
}
}
else
{
// Http status code indicates error
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
responseStream.Close();
response.Close();
var parseResult = ValidateJson(data);
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
if(error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
}
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString();
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
}
else
{
// Request timed out
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
}
}
}
/// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null
/// </summary>
/// <param name="data">Received data</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
{
return Task.FromResult<ServerError?>(null);
}
/// <summary>
/// Creates a request object
/// </summary>
/// <param name="apiClient">The API client the request is for</param>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed</param>
/// <param name="arraySerialization">How array parameters should be serialized</param>
/// <param name="requestId">Unique id of a request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
protected virtual IRequest ConstructRequest(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
Dictionary<string, object>? parameters,
bool signed,
HttpMethodParameterPosition parameterPosition,
ArrayParametersSerialization arraySerialization,
int requestId,
Dictionary<string, string>? additionalHeaders)
{
parameters ??= new Dictionary<string, object>();
for (var i = 0; i< parameters.Count; i++)
{
var kvp = parameters.ElementAt(i);
if (kvp.Value is Func<object> delegateValue)
parameters[kvp.Key] = delegateValue();
}
if (parameterPosition == HttpMethodParameterPosition.InUri)
{
foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
}
var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
if (apiClient.AuthenticationProvider != null)
apiClient.AuthenticationProvider.AuthenticateRequest(
apiClient,
uri,
method,
parameters,
signed,
arraySerialization,
parameterPosition,
out uriParameters,
out bodyParameters,
out headers);
// Sanity check
foreach(var param in parameters)
{
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
$"should return provided parameters in either the uri or body parameters output");
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
uri = uri.SetParameters(uriParameters, arraySerialization);
var request = RequestFactory.Create(method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
if (additionalHeaders != null)
{
foreach (var header in additionalHeaders)
request.AddHeader(header.Key, header.Value);
}
if (StandardRequestHeaders != null)
{
foreach (var header in StandardRequestHeaders)
// Only add it if it isn't overwritten
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = apiClient.requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Any())
WriteParamBody(apiClient, request, bodyParameters, contentType);
else
request.SetContent(apiClient.requestBodyEmptyContent, contentType);
}
return request;
}
/// <summary>
/// Writes the parameters of the request to the request object body
/// </summary>
/// <param name="apiClient">The client making the request</param>
/// <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(BaseApiClient apiClient, IRequest request, SortedDictionary<string, object> parameters, string contentType)
{
if (apiClient.requestBodyFormat == RequestBodyFormat.Json)
{
// Write the parameters as json in the body
var stringData = JsonConvert.SerializeObject(parameters);
request.SetContent(stringData, contentType);
}
else if (apiClient.requestBodyFormat == RequestBodyFormat.FormData)
{
// Write the parameters as form data in the body
var stringData = parameters.ToFormData();
request.SetContent(stringData, contentType);
}
}
/// <summary>
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
/// </summary>
/// <param name="error">The string the request returned</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(JToken error)
{
return new ServerError(error.ToString());
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
@ -19,7 +18,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Base for socket client implementations
/// </summary>
public abstract class SocketClient: BaseClient, ISocketClient
public abstract class BaseSocketClient: BaseClient, ISocketClient
{
#region fields
/// <summary>
@ -30,32 +29,15 @@ namespace CryptoExchange.Net
/// <summary>
/// List of socket connections currently connecting/connected
/// </summary>
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new ConcurrentDictionary<int, SocketConnection>();
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
/// <summary>
/// Semaphore used while creating sockets
/// </summary>
protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
/// <inheritdoc cref="SocketClientOptions.ReconnectInterval"/>
public TimeSpan ReconnectInterval { get; }
/// <inheritdoc cref="SocketClientOptions.AutoReconnect"/>
public bool AutoReconnect { get; }
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
public TimeSpan ResponseTimeout { get; }
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
public TimeSpan SocketNoDataTimeout { get; }
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
/// <summary>
/// The max amount of concurrent socket connections
/// Keep alive interval for websocket connection
/// </summary>
public int MaxSocketConnections { get; protected set; } = 9999;
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
public int SocketCombineTarget { get; protected set; }
/// <inheritdoc cref="SocketClientOptions.MaxReconnectTries"/>
public int? MaxReconnectTries { get; protected set; }
/// <inheritdoc cref="SocketClientOptions.MaxResubscribeTries"/>
public int? MaxResubscribeTries { get; protected set; }
/// <inheritdoc cref="SocketClientOptions.MaxConcurrentResubscriptionsPerSocket"/>
public int MaxConcurrentResubscriptionsPerSocket { get; protected set; }
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
@ -67,7 +49,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
/// </summary>
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new Dictionary<string, Action<MessageEvent>>();
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
/// <summary>
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
/// </summary>
@ -97,40 +79,54 @@ namespace CryptoExchange.Net
/// </summary>
protected internal int? RateLimitPerSocketPerSecond { get; set; }
/// <summary>
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
/// </summary>
/// <inheritdoc />
public double IncomingKbps
{
get
{
if (!sockets.Any())
if (!socketConnections.Any())
return 0;
return sockets.Sum(s => s.Value.Socket.IncomingKbps);
return socketConnections.Sum(s => s.Value.IncomingKbps);
}
}
/// <inheritdoc />
public int CurrentConnections => socketConnections.Count;
/// <inheritdoc />
public int CurrentSubscriptions
{
get
{
if (!socketConnections.Any())
return 0;
return socketConnections.Sum(s => s.Value.SubscriptionCount);
}
}
/// <summary>
/// Client options
/// </summary>
public new BaseSocketClientOptions ClientOptions { get; }
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="exchangeName">The name of the exchange this client is for</param>
/// <param name="exchangeOptions">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 SocketClient(string exchangeName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeName, exchangeOptions, authenticationProvider)
/// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param>
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
{
if (exchangeOptions == null)
throw new ArgumentNullException(nameof(exchangeOptions));
ClientOptions = options ?? throw new ArgumentNullException(nameof(options));
}
AutoReconnect = exchangeOptions.AutoReconnect;
ReconnectInterval = exchangeOptions.ReconnectInterval;
ResponseTimeout = exchangeOptions.SocketResponseTimeout;
SocketNoDataTimeout = exchangeOptions.SocketNoDataTimeout;
SocketCombineTarget = exchangeOptions.SocketSubscriptionsCombineTarget ?? 1;
MaxReconnectTries = exchangeOptions.MaxReconnectTries;
MaxResubscribeTries = exchangeOptions.MaxResubscribeTries;
MaxConcurrentResubscriptionsPerSocket = exchangeOptions.MaxConcurrentResubscriptionsPerSocket;
/// <inheritdoc />
public void SetApiCredentials(ApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
@ -148,56 +144,83 @@ namespace CryptoExchange.Net
/// Connect to an url and listen for data on the BaseAddress
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="apiClient">The API client the subscription is for</param>
/// <param name="request">The optional request object to send, will be serialized to json</param>
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
/// <param name="dataHandler">The handler of update data</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
{
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler);
return SubscribeAsync(apiClient, apiClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
}
/// <summary>
/// Connect to an url and listen for data
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="apiClient">The API client the subscription is for</param>
/// <param name="url">The URL to connect to</param>
/// <param name="request">The optional request object to send, will be serialized to json</param>
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
/// <param name="dataHandler">The handler of update data</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
{
if (disposing)
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
SocketConnection socketConnection;
SocketSubscription subscription;
SocketSubscription? subscription;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
// Get a new or existing socket connection
socketConnection = GetSocketConnection(url, authenticated);
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
}
// Add a subscription on the socket connection
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
if (SocketCombineTarget == 1)
try
{
while (true)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
if(!socketResult)
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
if (subscription == null)
{
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(connectResult.Error!);
break;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(null, connectResult.Error);
if (needsConnecting)
log.Write(LogLevel.Debug, $"Socket {socketConnection.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}");
}
finally
{
@ -207,8 +230,8 @@ namespace CryptoExchange.Net
if (socketConnection.PausedActivity)
{
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} has been paused, can't subscribe at this moment");
return new CallResult<UpdateSubscription>(default, new ServerError("Socket is paused"));
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
return new CallResult<UpdateSubscription>( new ServerError("Socket is paused"));
}
if (request != null)
@ -217,8 +240,9 @@ namespace CryptoExchange.Net
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
if (!subResult)
{
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(null, subResult.Error);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
else
@ -227,8 +251,17 @@ namespace CryptoExchange.Net
subscription.Confirmed = true;
}
socketConnection.ShouldReconnect = true;
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription), null);
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
@ -241,43 +274,59 @@ namespace CryptoExchange.Net
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{
CallResult<object>? callResult = null;
await socketConnection.SendAndWaitAsync(request, ResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
await socketConnection.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true)
{
subscription.Confirmed = true;
return new CallResult<bool>(true);
}
return new CallResult<bool>(callResult?.Success ?? false, callResult == null ? new ServerError("No response on subscription request received"): callResult.Error);
if(callResult== null)
return new CallResult<bool>(new ServerError("No response on subscription request received"));
return new CallResult<bool>(callResult.Error!);
}
/// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary>
/// <typeparam name="T">Expected result type</typeparam>
/// <param name="apiClient">The API client the query is for</param>
/// <param name="request">The request to send, will be serialized to json</param>
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
/// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
protected virtual Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, object request, bool authenticated)
{
return QueryAsync<T>(BaseAddress, request, authenticated);
return QueryAsync<T>(apiClient, apiClient.Options.BaseAddress, request, authenticated);
}
/// <summary>
/// Send a query on a socket connection and wait for the response
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <param name="apiClient">The API client the query is for</param>
/// <param name="url">The url for the request</param>
/// <param name="request">The request to send</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
protected virtual async Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, string url, object request, bool authenticated)
{
if (disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
SocketConnection socketConnection;
var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
socketConnection = GetSocketConnection(url, authenticated);
if (SocketCombineTarget == 1)
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<T>(default);
socketConnection = socketResult.Data;
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Can release early when only a single sub per connection
semaphoreSlim.Release();
@ -286,20 +335,18 @@ namespace CryptoExchange.Net
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(default, connectResult.Error);
return new CallResult<T>(connectResult.Error!);
}
finally
{
//When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
if (!released)
semaphoreSlim.Release();
}
if (socketConnection.PausedActivity)
{
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} has been paused, can't send query at this moment");
return new CallResult<T>(default, new ServerError("Socket is paused"));
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
return new CallResult<T>(new ServerError("Socket is paused"));
}
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
@ -314,8 +361,8 @@ namespace CryptoExchange.Net
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
{
var dataResult = new CallResult<T>(default, new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, ResponseTimeout, data =>
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data =>
{
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false;
@ -336,25 +383,29 @@ namespace CryptoExchange.Net
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
{
if (socket.Connected)
return new CallResult<bool>(true, null);
return new CallResult<bool>(true);
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
if (!connectResult)
return new CallResult<bool>(false, connectResult.Error);
return new CallResult<bool>(connectResult.Error!);
if (!authenticated || socket.Authenticated)
return new CallResult<bool>(true, null);
return new CallResult<bool>(true);
log.Write(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
{
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} authentication failed");
log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
if(socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(false, result.Error);
return new CallResult<bool>(result.Error);
}
socket.Authenticated = true;
return new CallResult<bool>(true, null);
return new CallResult<bool>(true);
}
/// <summary>
@ -389,18 +440,20 @@ namespace CryptoExchange.Net
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
/// </summary>
/// <param name="socketConnection">The socket connection the message was recieved on</param>
/// <param name="message">The received data</param>
/// <param name="request">The subscription request</param>
/// <returns>True if the message is for the subscription which sent the request</returns>
protected internal abstract bool MessageMatchesHandler(JToken message, object request);
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
/// <summary>
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
/// </summary>
/// <param name="socketConnection">The socket connection the message was recieved on</param>
/// <param name="message">The received data</param>
/// <param name="identifier">The string identifier of the handler</param>
/// <returns>True if the message is for the handler which has the identifier</returns>
protected internal abstract bool MessageMatchesHandler(JToken message, string identifier);
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
/// <summary>
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
/// </summary>
@ -434,32 +487,34 @@ namespace CryptoExchange.Net
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
/// <param name="connection">The socket connection the handler is on</param>
/// <param name="dataHandler">The handler of the data received</param>
/// <param name="authenticated">Whether the subscription needs authentication</param>
/// <returns></returns>
protected virtual SocketSubscription AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler)
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
{
void InternalHandler(MessageEvent messageEvent)
{
if (typeof(T) == typeof(string))
{
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
dataHandler(new DataEvent<T>(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
dataHandler(new DataEvent<T>(stringData, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
return;
}
var desResult = Deserialize<T>(messageEvent.JsonData, false);
var desResult = Deserialize<T>(messageEvent.JsonData);
if (!desResult)
{
log.Write(LogLevel.Warning, $"Socket {connection.Socket.Id} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
return;
}
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
dataHandler(new DataEvent<T>(desResult.Data, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
}
var subscription = request == null
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, InternalHandler)
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, InternalHandler);
connection.AddSubscription(subscription);
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
if (!connection.AddSubscription(subscription))
return null;
return subscription;
}
@ -467,46 +522,82 @@ namespace CryptoExchange.Net
/// Adds a generic message handler. Used for example to reply to ping requests
/// </summary>
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(Newtonsoft.Json.Linq.JToken,string)"/>)</param>
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
{
genericHandlers.Add(identifier, action);
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action);
foreach (var connection in sockets.Values)
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
foreach (var connection in socketConnections.Values)
connection.AddSubscription(subscription);
}
/// <summary>
/// Get the url to connect to (defaults to BaseAddress form the client options)
/// </summary>
/// <param name="apiClient"></param>
/// <param name="address"></param>
/// <param name="authentication"></param>
/// <returns></returns>
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(SocketApiClient apiClient, string address, bool authentication)
{
return Task.FromResult(new CallResult<string?>(address));
}
/// <summary>
/// Get the url to reconnect to after losing a connection
/// </summary>
/// <param name="apiClient"></param>
/// <param name="connection"></param>
/// <returns></returns>
public virtual Task<Uri?> GetReconnectUriAsync(SocketApiClient apiClient, SocketConnection connection)
{
return Task.FromResult<Uri?>(connection.ConnectionUri);
}
/// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary>
/// <param name="apiClient">The API client the connection is for</param>
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <returns></returns>
protected virtual SocketConnection GetSocketConnection(string address, bool authenticated)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
{
var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/')
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
if (result != null)
{
if (result.SubscriptionCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= SocketCombineTarget)))
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= ClientOptions.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return result;
return new CallResult<SocketConnection>(result);
}
}
var connectionAddress = await GetConnectionUrlAsync(apiClient, address, authenticated).ConfigureAwait(false);
if (!connectionAddress)
{
log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
return connectionAddress.As<SocketConnection>(null);
}
if (connectionAddress.Data != address)
log.Write(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
// Create new socket
var socket = CreateSocket(address);
var socketConnection = new SocketConnection(this, socket);
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(this, apiClient, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
foreach (var kvp in genericHandlers)
{
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, kvp.Value);
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
socketConnection.AddSubscription(handler);
}
return socketConnection;
return new CallResult<SocketConnection>(socketConnection);
}
/// <summary>
@ -524,16 +615,33 @@ namespace CryptoExchange.Net
/// <returns></returns>
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
{
if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false))
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
{
sockets.TryAdd(socketConnection.Socket.Id, socketConnection);
return new CallResult<bool>(true, null);
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
return new CallResult<bool>(true);
}
socketConnection.Socket.Dispose();
return new CallResult<bool>(false, new CantConnectError());
socketConnection.Dispose();
return new CallResult<bool>(new CantConnectError());
}
/// <summary>
/// Get parameters for the websocket connection
/// </summary>
/// <param name="address">The address to connect to</param>
/// <returns></returns>
protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new (new Uri(address), ClientOptions.AutoReconnect)
{
DataInterpreterBytes = dataInterpreterBytes,
DataInterpreterString = dataInterpreterString,
KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval,
RatelimitPerSecond = RateLimitPerSocketPerSecond,
Proxy = ClientOptions.Proxy,
Timeout = ClientOptions.SocketNoDataTimeout
};
/// <summary>
/// Create a socket for an address
/// </summary>
@ -541,32 +649,18 @@ namespace CryptoExchange.Net
/// <returns></returns>
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(log, address);
var socket = SocketFactory.CreateWebsocket(log, GetWebSocketParameters(address));
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
if (apiProxy != null)
socket.SetProxy(apiProxy);
socket.Timeout = SocketNoDataTimeout;
socket.DataInterpreterBytes = dataInterpreterBytes;
socket.DataInterpreterString = dataInterpreterString;
socket.RatelimitPerSecond = RateLimitPerSocketPerSecond;
socket.OnError += e =>
{
if(e is WebSocketException wse)
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
else
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: " + e.ToLogString());
};
return socket;
}
/// <summary>
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="identifier">Identifier for the periodic send</param>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
public virtual void SendPeriodic(TimeSpan interval, Func<SocketConnection, object> objGetter)
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
{
if (objGetter == null)
throw new ArgumentNullException(nameof(objGetter));
@ -580,27 +674,27 @@ namespace CryptoExchange.Net
if (disposing)
break;
foreach (var socket in sockets.Values)
foreach (var socketConnection in socketConnections.Values)
{
if (disposing)
break;
if (!socket.Socket.IsOpen)
if (!socketConnection.Connected)
continue;
var obj = objGetter(socket);
var obj = objGetter(socketConnection);
if (obj == null)
continue;
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic");
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
try
{
socket.Send(obj);
socketConnection.Send(obj);
}
catch (Exception ex)
{
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} Periodic send failed: " + ex);
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
}
}
}
@ -614,10 +708,9 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
SocketSubscription? subscription = null;
SocketConnection? connection = null;
foreach(var socket in sockets.Values.ToList())
foreach(var socket in socketConnections.Values.ToList())
{
subscription = socket.GetSubscription(subscriptionId);
if (subscription != null)
@ -630,7 +723,7 @@ namespace CryptoExchange.Net
if (subscription == null || connection == null)
return;
log.Write(LogLevel.Information, "Closing subscription " + subscriptionId);
log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
await connection.CloseAsync(subscription).ConfigureAwait(false);
}
@ -644,7 +737,7 @@ namespace CryptoExchange.Net
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
log.Write(LogLevel.Information, "Closing subscription " + subscription.Id);
log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
@ -654,19 +747,48 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
log.Write(LogLevel.Debug, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions");
await Task.Run(async () =>
log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
var tasks = new List<Task>();
{
var tasks = new List<Task>();
{
var socketList = sockets.Values;
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
}
var socketList = socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}).ConfigureAwait(false);
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
log.Write(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.TriggerReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState()
{
var sb = new StringBuilder();
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
foreach(var connection in socketConnections)
{
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
foreach (var subscription in connection.Value.Subscriptions)
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
}
return sb.ToString();
}
/// <summary>
@ -677,8 +799,11 @@ namespace CryptoExchange.Net
disposing = true;
periodicEvent?.Set();
periodicEvent?.Dispose();
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult();
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
{
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
_ = UnsubscribeAllAsync();
}
semaphoreSlim?.Dispose();
base.Dispose();
}

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net
{
/// <summary>
/// Base rest API client for interacting with a REST API
/// </summary>
public abstract class RestApiClient: BaseApiClient
{
/// <summary>
/// Get time sync info for an API client
/// </summary>
/// <returns></returns>
public abstract TimeSyncInfo GetTimeSyncInfo();
/// <summary>
/// Get time offset for an API client
/// </summary>
/// <returns></returns>
public abstract TimeSpan GetTimeOffset();
/// <summary>
/// Total amount of requests made with this API client
/// </summary>
public int TotalRequestsMade { get; set; }
/// <summary>
/// Options for this client
/// </summary>
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
/// <summary>
/// List of rate limiters
/// </summary>
internal IEnumerable<IRateLimiter> RateLimiters { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
{
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
}
/// <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 abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
}
var localTime = DateTime.UtcNow;
var result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.As(false);
}
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.As(false);
}
}
// 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 new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
}
}
}

View File

@ -0,0 +1,19 @@
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net
{
/// <summary>
/// Base socket API client for interaction with a websocket API
/// </summary>
public abstract class SocketApiClient : BaseApiClient
{
/// <summary>
/// ctor
/// </summary>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
{
}
}
}

View File

@ -0,0 +1,21 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Balance data
/// </summary>
public class Balance: BaseCommonObject
{
/// <summary>
/// The asset name
/// </summary>
public string Asset { get; set; } = string.Empty;
/// <summary>
/// Quantity available
/// </summary>
public decimal? Available { get; set; }
/// <summary>
/// Total quantity
/// </summary>
public decimal? Total { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Base class for common objects
/// </summary>
public class BaseCommonObject
{
/// <summary>
/// The source object the data is derived from
/// </summary>
public object SourceObject { get; set; } = null!;
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order type
/// </summary>
public enum CommonOrderType
{
/// <summary>
/// Limit type
/// </summary>
Limit,
/// <summary>
/// Market type
/// </summary>
Market,
/// <summary>
/// Other order type
/// </summary>
Other
}
/// <summary>
/// Order side
/// </summary>
public enum CommonOrderSide
{
/// <summary>
/// Buy order
/// </summary>
Buy,
/// <summary>
/// Sell order
/// </summary>
Sell
}
/// <summary>
/// Order status
/// </summary>
public enum CommonOrderStatus
{
/// <summary>
/// placed and not fully filled order
/// </summary>
Active,
/// <summary>
/// canceled order
/// </summary>
Canceled,
/// <summary>
/// filled order
/// </summary>
Filled
}
/// <summary>
/// Position side
/// </summary>
public enum CommonPositionSide
{
/// <summary>
/// Long position
/// </summary>
Long,
/// <summary>
/// Short position
/// </summary>
Short,
/// <summary>
/// Both
/// </summary>
Both
}
}

View File

@ -0,0 +1,35 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Kline data
/// </summary>
public class Kline: BaseCommonObject
{
/// <summary>
/// Opening time of the kline
/// </summary>
public DateTime OpenTime { get; set; }
/// <summary>
/// Price at the open time
/// </summary>
public decimal? OpenPrice { get; set; }
/// <summary>
/// Highest price of the kline
/// </summary>
public decimal? HighPrice { get; set; }
/// <summary>
/// Lowest price of the kline
/// </summary>
public decimal? LowPrice { get; set; }
/// <summary>
/// Close price of the kline
/// </summary>
public decimal? ClosePrice { get; set; }
/// <summary>
/// Volume of the kline
/// </summary>
public decimal? Volume { get; set; }
}
}

View File

@ -0,0 +1,47 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order data
/// </summary>
public class Order: BaseCommonObject
{
/// <summary>
/// Id of the order
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Symbol of the order
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Price of the order
/// </summary>
public decimal? Price { get; set; }
/// <summary>
/// Quantity of the order
/// </summary>
public decimal? Quantity { get; set; }
/// <summary>
/// The quantity of the order which has been filled
/// </summary>
public decimal? QuantityFilled { get; set; }
/// <summary>
/// Status of the order
/// </summary>
public CommonOrderStatus Status { get; set; }
/// <summary>
/// Side of the order
/// </summary>
public CommonOrderSide Side { get; set; }
/// <summary>
/// Type of the order
/// </summary>
public CommonOrderType Type { get; set; }
/// <summary>
/// Order time
/// </summary>
public DateTime Timestamp { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order book data
/// </summary>
public class OrderBook: BaseCommonObject
{
/// <summary>
/// List of bids
/// </summary>
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
/// <summary>
/// List of asks
/// </summary>
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
}
}

View File

@ -0,0 +1,17 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order book entry
/// </summary>
public class OrderBookEntry
{
/// <summary>
/// Quantity of the entry
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Price of the entry
/// </summary>
public decimal Price { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Id of an order
/// </summary>
public class OrderId: BaseCommonObject
{
/// <summary>
/// Id of an order
/// </summary>
public string Id { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,65 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Position data
/// </summary>
public class Position: BaseCommonObject
{
/// <summary>
/// Id of the position
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Symbol of the position
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Leverage
/// </summary>
public decimal Leverage { get; set; }
/// <summary>
/// Position quantity
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Entry price
/// </summary>
public decimal? EntryPrice { get; set; }
/// <summary>
/// Liquidation price
/// </summary>
public decimal? LiquidationPrice { get; set; }
/// <summary>
/// Unrealized profit and loss
/// </summary>
public decimal? UnrealizedPnl { get; set; }
/// <summary>
/// Realized profit and loss
/// </summary>
public decimal? RealizedPnl { get; set; }
/// <summary>
/// Mark price
/// </summary>
public decimal? MarkPrice { get; set; }
/// <summary>
/// Auto adding margin
/// </summary>
public bool? AutoMargin { get; set; }
/// <summary>
/// Position margin
/// </summary>
public decimal? PositionMargin { get; set; }
/// <summary>
/// Position side
/// </summary>
public CommonPositionSide? Side { get; set; }
/// <summary>
/// Is isolated
/// </summary>
public bool? Isolated { get; set; }
/// <summary>
/// Maintenance margin
/// </summary>
public decimal? MaintananceMargin { get; set; }
}
}

View File

@ -0,0 +1,33 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Symbol data
/// </summary>
public class Symbol: BaseCommonObject
{
/// <summary>
/// Name of the symbol
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Minimal quantity of an order
/// </summary>
public decimal? MinTradeQuantity { get; set; }
/// <summary>
/// Step with which the quantity should increase
/// </summary>
public decimal? QuantityStep { get; set; }
/// <summary>
/// step with which the price should increase
/// </summary>
public decimal? PriceStep { get; set; }
/// <summary>
/// The max amount of decimals for quantity
/// </summary>
public int? QuantityDecimals { get; set; }
/// <summary>
/// The max amount of decimal for price
/// </summary>
public int? PriceDecimals { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Ticker data
/// </summary>
public class Ticker: BaseCommonObject
{
/// <summary>
/// Symbol
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Price 24 hours ago
/// </summary>
public decimal? Price24H { get; set; }
/// <summary>
/// Last trade price
/// </summary>
public decimal? LastPrice { get; set; }
/// <summary>
/// 24 hour low price
/// </summary>
public decimal? LowPrice { get; set; }
/// <summary>
/// 24 hour high price
/// </summary>
public decimal? HighPrice { get; set; }
/// <summary>
/// 24 hour volume
/// </summary>
public decimal? Volume { get; set; }
}
}

View File

@ -0,0 +1,50 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Trade data
/// </summary>
public class Trade: BaseCommonObject
{
/// <summary>
/// Symbol of the trade
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Price of the trade
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// Quantity of the trade
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Timestamp of the trade
/// </summary>
public DateTime Timestamp { get; set; }
}
/// <summary>
/// User trade info
/// </summary>
public class UserTrade: Trade
{
/// <summary>
/// Id of the trade
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Order id of the trade
/// </summary>
public string? OrderId { get; set; }
/// <summary>
/// Fee of the trade
/// </summary>
public decimal? Fee { get; set; }
/// <summary>
/// The asset the fee is paid in
/// </summary>
public string? FeeAsset { get; set; }
}
}

View File

@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters
return ParseObject(arr, result, objectType);
}
private static object? ParseObject(JArray arr, object result, Type objectType)
private static object ParseObject(JArray arr, object result, Type objectType)
{
foreach (var property in objectType.GetProperties())
{
@ -63,8 +63,8 @@ namespace CryptoExchange.Net.Converters
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);
var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
count++;
}
property.SetValue(result, arrayResult);
@ -72,8 +72,8 @@ namespace CryptoExchange.Net.Converters
else
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
var innerObj = Activator.CreateInstance(objType);
arrayResult[0] = ParseObject(innerArray, innerObj, objType);
var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
property.SetValue(result, arrayResult);
}
continue;
@ -181,6 +181,7 @@ namespace CryptoExchange.Net.Converters
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute: Attribute
{
/// <summary>

View File

@ -43,9 +43,13 @@ namespace CryptoExchange.Net.Converters
if (reader.Value == null)
return null;
if (!GetValue(reader.Value.ToString(), out var result))
var stringValue = reader.Value.ToString();
if (string.IsNullOrWhiteSpace(stringValue))
return null;
if (!GetValue(stringValue, out var result))
{
Debug.WriteLine($"Cannot map enum. Type: {typeof(T)}, Value: {reader.Value}");
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
return null;
}
@ -71,7 +75,7 @@ namespace CryptoExchange.Net.Converters
private bool GetValue(string value, out T result)
{
//check for exact match first, then if not found fallback to a case insensitive match
// 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));

View File

@ -0,0 +1,198 @@
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
/// </summary>
public class DateTimeConverter: JsonConverter
{
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 / 1000;
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
if(reader.TokenType is JsonToken.Integer)
{
var longValue = (long)reader.Value;
if (longValue == 0 || longValue == -1)
return objectType == typeof(DateTime) ? default(DateTime): null;
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
return ConvertFromNanoseconds(longValue);
}
else if (reader.TokenType is JsonToken.Float)
{
var doubleValue = (double)reader.Value;
if (doubleValue == 0 || doubleValue == -1)
return objectType == typeof(DateTime) ? default(DateTime) : null;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
return ConvertFromMilliseconds(doubleValue);
}
else if(reader.TokenType is JsonToken.String)
{
var stringValue = (string)reader.Value;
if (string.IsNullOrWhiteSpace(stringValue))
return null;
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
return objectType == typeof(DateTime) ? default(DateTime) : null;
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))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
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))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
// Parse 1637745563.000 format
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue);
if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue);
return ConvertFromNanoseconds((long)doubleValue);
}
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))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
else if(reader.TokenType == JsonToken.Date)
{
return (DateTime)reader.Value;
}
else
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * ticksPerSecond));
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="nanoseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * ticksPerNanosecond));
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[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>
/// <param name="time"></param>
/// <returns></returns>
[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>
/// <param name="time"></param>
/// <returns></returns>
[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>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerNanosecond);
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var datetimeValue = (DateTime?)value;
if (datetimeValue == null)
writer.WriteValue((DateTime?)null);
if(datetimeValue == default(DateTime))
writer.WriteValue((DateTime?)null);
else
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
}

View File

@ -0,0 +1,138 @@
using CryptoExchange.Net.Attributes;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary>
public class EnumConverter : JsonConverter
{
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType.IsEnum;
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (!_mapping.TryGetValue(enumType, out var mapping))
mapping = AddMapping(enumType);
var stringValue = reader.Value?.ToString();
if (stringValue == null)
{
// Received null value
var emptyResult = GetDefaultValue(objectType, enumType);
if(emptyResult != null)
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
return emptyResult;
}
if (!GetValue(enumType, mapping, stringValue!, out var result))
{
var defaultValue = GetDefaultValue(objectType, enumType);
if (string.IsNullOrWhiteSpace(stringValue))
{
if (defaultValue != null)
// We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
}
else
// We received an enum value but weren't able to parse it.
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
return defaultValue;
}
return result;
}
private static object? GetDefaultValue(Type objectType, Type enumType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return null;
return Activator.CreateInstance(enumType); // return default value
}
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
{
var mapping = new List<KeyValuePair<object, string>>();
var enumMembers = objectType.GetMembers();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
}
}
_mapping.TryAdd(objectType, mapping);
return mapping;
}
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
{
// Check for exact match first, then if not found fallback to a case insensitive match
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>)))
{
result = mapping.Key;
return true;
}
try
{
// If no explicit mapping is found try to parse string
result = Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
/// <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>
/// <typeparam name="T"></typeparam>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T enumValue)
{
var objectType = typeof(T);
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (!_mapping.TryGetValue(objectType, out var mapping))
mapping = AddMapping(objectType);
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var stringValue = GetString(value);
writer.WriteValue(stringValue);
}
}
}

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

@ -5,19 +5,19 @@
<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>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>5.2.4</PackageVersion>
<AssemblyVersion>5.2.4</AssemblyVersion>
<FileVersion>5.2.4</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<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>5.2.4 - Added handling of PlatformNotSupportedException when trying to use websocket from WebAssembly, Changed DataEvent to have a public constructor for testing purposes, Fixed EnumConverter serializing values without proper quotes, Fixed websocket connection reconnecting too quickly when resubscribing/reauthenticating fails</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
<LangVersion>9.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
@ -41,11 +41,14 @@
<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="6.0.0">
<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" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

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,35 +0,0 @@
using System;
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common kline
/// </summary>
public interface ICommonKline
{
/// <summary>
/// High price for this kline
/// </summary>
decimal CommonHigh { get; }
/// <summary>
/// Low price for this kline
/// </summary>
decimal CommonLow { get; }
/// <summary>
/// Open price for this kline
/// </summary>
decimal CommonOpen { get; }
/// <summary>
/// Close price for this kline
/// </summary>
decimal CommonClose { get; }
/// <summary>
/// Open time for this kline
/// </summary>
DateTime CommonOpenTime { get; }
/// <summary>
/// Volume of this kline
/// </summary>
decimal CommonVolume { get; }
}
}

View File

@ -1,43 +0,0 @@
using System;
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common order
/// </summary>
public interface ICommonOrder: ICommonOrderId
{
/// <summary>
/// Symbol of the order
/// </summary>
public string CommonSymbol { get; }
/// <summary>
/// Price of the order
/// </summary>
public decimal CommonPrice { get; }
/// <summary>
/// Quantity of the order
/// </summary>
public decimal CommonQuantity { get; }
/// <summary>
/// Status of the order
/// </summary>
public IExchangeClient.OrderStatus CommonStatus { get; }
/// <summary>
/// Whether the order is active
/// </summary>
public bool IsActive { get; }
/// <summary>
/// Side of the order
/// </summary>
public IExchangeClient.OrderSide CommonSide { get; }
/// <summary>
/// Type of the order
/// </summary>
public IExchangeClient.OrderType CommonType { get; }
/// <summary>
/// order time
/// </summary>
DateTime CommonOrderTime { get; }
}
}

View File

@ -1,20 +0,0 @@
using System.Collections.Generic;
using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common order book
/// </summary>
public interface ICommonOrderBook
{
/// <summary>
/// Bids
/// </summary>
IEnumerable<ISymbolOrderBookEntry> CommonBids { get; }
/// <summary>
/// Asks
/// </summary>
IEnumerable<ISymbolOrderBookEntry> CommonAsks { get; }
}
}

View File

@ -1,13 +0,0 @@
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common order id
/// </summary>
public interface ICommonOrderId
{
/// <summary>
/// Id of the order
/// </summary>
public string CommonId { get; }
}
}

View File

@ -1,23 +0,0 @@
using System;
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Recent trade
/// </summary>
public interface ICommonRecentTrade
{
/// <summary>
/// Price of the trade
/// </summary>
decimal CommonPrice { get; }
/// <summary>
/// Quantity of the trade
/// </summary>
decimal CommonQuantity { get; }
/// <summary>
/// Trade time
/// </summary>
DateTime CommonTradeTime { get; }
}
}

View File

@ -1,17 +0,0 @@
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common symbol
/// </summary>
public interface ICommonSymbol
{
/// <summary>
/// Symbol name
/// </summary>
public string CommonName { get; }
/// <summary>
/// Minimum trade size
/// </summary>
public decimal CommonMinimumTradeSize { get; }
}
}

View File

@ -1,25 +0,0 @@
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common ticker
/// </summary>
public interface ICommonTicker
{
/// <summary>
/// Symbol name
/// </summary>
public string CommonSymbol { get; }
/// <summary>
/// High price
/// </summary>
public decimal CommonHigh { get; }
/// <summary>
/// Low price
/// </summary>
public decimal CommonLow { get; }
/// <summary>
/// Volume
/// </summary>
public decimal CommonVolume { get; }
}
}

View File

@ -5,8 +5,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
@ -74,7 +73,7 @@ namespace CryptoExchange.Net
/// <param name="value"></param>
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
{
if(value != null)
if (value != null)
parameters.Add(key, value);
}
@ -129,7 +128,7 @@ namespace CryptoExchange.Net
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
foreach (var arrayEntry in arraysParameters)
{
if(serializationType == ArrayParametersSerialization.Array)
if (serializationType == ArrayParametersSerialization.Array)
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
else
{
@ -144,6 +143,29 @@ namespace CryptoExchange.Net
return uriString;
}
/// <summary>
/// Convert a dictionary to formdata string
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
public static string ToFormData(this SortedDictionary<string, object> parameters)
{
var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters)
{
if (kvp.Value.GetType().IsArray)
{
var array = (Array)kvp.Value;
foreach (var value in array)
formData.Add(kvp.Key, value.ToString());
}
else
formData.Add(kvp.Key, kvp.Value.ToString());
}
return formData.ToString();
}
/// <summary>
/// Get the string the secure string is representing
/// </summary>
@ -177,6 +199,41 @@ namespace CryptoExchange.Net
}
}
/// <summary>
/// Are 2 secure strings equal
/// </summary>
/// <param name="ss1">Source secure string</param>
/// <param name="ss2">Compare secure string</param>
/// <returns>True if equal by value</returns>
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
{
IntPtr bstr1 = IntPtr.Zero;
IntPtr bstr2 = IntPtr.Zero;
try
{
bstr1 = Marshal.SecureStringToBSTR(ss1);
bstr2 = Marshal.SecureStringToBSTR(ss2);
int length1 = Marshal.ReadInt32(bstr1, -4);
int length2 = Marshal.ReadInt32(bstr2, -4);
if (length1 == length2)
{
for (int x = 0; x < length1; ++x)
{
byte b1 = Marshal.ReadByte(bstr1, x);
byte b2 = Marshal.ReadByte(bstr2, x);
if (b1 != b2) return false;
}
}
else return false;
return true;
}
finally
{
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
}
}
/// <summary>
/// Create a secure string from a string
/// </summary>
@ -210,14 +267,14 @@ namespace CryptoExchange.Net
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
log?.Write(LogLevel.Error, info);
if (log == null) Debug.WriteLine(info);
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null;
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
log?.Write(LogLevel.Error, info);
if (log == null) Debug.WriteLine(info);
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null;
}
}
@ -298,7 +355,7 @@ namespace CryptoExchange.Net
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public static string ToLogString(this Exception exception)
public static string ToLogString(this Exception? exception)
{
var message = new StringBuilder();
var indent = 0;
@ -319,6 +376,122 @@ namespace CryptoExchange.Net
return message.ToString();
}
/// <summary>
/// Append a base url with provided path
/// </summary>
/// <param name="url"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string AppendPath(this string url, params string[] path)
{
if (!url.EndsWith("/"))
url += "/";
foreach (var item in path)
url += item.Trim('/') + "/";
return url.TrimEnd('/');
}
/// <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>
public static string FillPathParameters(this 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>
/// Create a new uri with the provided parameters as query
/// </summary>
/// <param name="parameters"></param>
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
{
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = baseUri.Scheme;
uriBuilder.Host = baseUri.Host;
uriBuilder.Port = baseUri.Port;
uriBuilder.Path = baseUri.AbsolutePath;
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
{
if(parameter.Value.GetType().IsArray)
{
foreach (var item in (object[])parameter.Value)
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
}
else
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Create a new uri with the provided parameters as query
/// </summary>
/// <param name="parameters"></param>
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
{
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = baseUri.Scheme;
uriBuilder.Host = baseUri.Host;
uriBuilder.Port = baseUri.Port;
uriBuilder.Path = baseUri.AbsolutePath;
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
{
if (parameter.Value.GetType().IsArray)
{
foreach (var item in (object[])parameter.Value)
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
}
else
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Add parameter to URI
/// </summary>
/// <param name="uri"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
{
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
httpValueCollection.Remove(name);
httpValueCollection.Add(name, value);
var ub = new UriBuilder(uri);
ub.Query = httpValueCollection.ToString();
return ub.Uri;
}
}
}

View File

@ -1,50 +1,60 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.ExchangeInterfaces
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Shared interface for exchange wrappers based on the CryptoExchange.Net package
/// Common rest client endpoints
/// </summary>
public interface IExchangeClient
public interface IBaseRestClient
{
/// <summary>
/// The name of the exchange
/// </summary>
string ExchangeName { get; }
/// <summary>
/// Should be triggered on order placing
/// </summary>
event Action<ICommonOrderId> OnOrderPlaced;
event Action<OrderId> OnOrderPlaced;
/// <summary>
/// Should be triggered on order cancelling
/// </summary>
event Action<ICommonOrderId> OnOrderCanceled;
event Action<OrderId> OnOrderCanceled;
/// <summary>
/// Get the symbol name based on a base and quote asset
/// </summary>
/// <param name="baseAsset"></param>
/// <param name="quoteAsset"></param>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <returns></returns>
string GetSymbolName(string baseAsset, string quoteAsset);
/// <summary>
/// Get a list of symbols for the exchange
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonSymbol>>> GetSymbolsAsync();
/// <summary>
/// Get a list of tickers for the exchange
/// </summary>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonTicker>>> GetTickersAsync();
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
/// <summary>
/// Get a ticker for the exchange
/// </summary>
/// <param name="symbol">The symbol to get klines for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<ICommonTicker>> GetTickerAsync(string symbol);
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// Get a list of tickers for the exchange
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
/// <summary>
/// Get a list of candles for a given symbol on the exchange
@ -54,124 +64,75 @@ namespace CryptoExchange.Net.ExchangeInterfaces
/// <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>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonKline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
/// <summary>
/// Get the order book for a symbol
/// </summary>
/// <param name="symbol">The symbol to get the book for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<ICommonOrderBook>> GetOrderBookAsync(string symbol);
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// The recent trades for a symbol
/// </summary>
/// <param name="symbol">The symbol to get the trades for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</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);
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// Get balances
/// </summary>
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<ICommonBalance>>> GetBalancesAsync(string? accountId = null);
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
/// <summary>
/// Common order id
/// Get an order by id
/// </summary>
public enum OrderType
{
/// <summary>
/// Limit type
/// </summary>
Limit,
/// <summary>
/// Market type
/// </summary>
Market,
/// <summary>
/// Other order type
/// </summary>
Other
}
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Common order side
/// Get trades for an order by id
/// </summary>
public enum OrderSide
{
/// <summary>
/// Buy order
/// </summary>
Buy,
/// <summary>
/// Sell order
/// </summary>
Sell
}
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Common order status
/// Get a list of open orders
/// </summary>
public enum OrderStatus
{
/// <summary>
/// placed and not fully filled order
/// </summary>
Active,
/// <summary>
/// cancelled order
/// </summary>
Canceled,
/// <summary>
/// filled order
/// </summary>
Filled
}
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <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>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <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>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
}
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common futures endpoints
/// </summary>
public interface IFuturesClient : IBaseRestClient
{
/// <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>
/// <param name="leverage">[Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand)</param>
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
/// <summary>
/// Get position
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
}
}

View File

@ -0,0 +1,28 @@
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common spot endpoints
/// </summary>
public interface ISpotClient: IBaseRestClient
{
/// <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>
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
}
}

View File

@ -1,4 +1,9 @@
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
@ -8,13 +13,17 @@ namespace CryptoExchange.Net.Interfaces
public interface IRateLimiter
{
/// <summary>
/// Limit the request if needed
/// Limit a request based on previous requests made
/// </summary>
/// <param name="client"></param>
/// <param name="url"></param>
/// <param name="limitBehaviour"></param>
/// <param name="credits"></param>
/// <returns></returns>
CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits=1);
/// <param name="log">The logger</param>
/// <param name="endpoint">The endpoint the request is for</param>
/// <param name="method">The Http request method</param>
/// <param name="signed">Whether the request is singed(private) or not</param>
/// <param name="apiKey">The api key making this request</param>
/// <param name="limitBehaviour">The limit behavior for when the limit is reached</param>
/// <param name="requestWeight">The weight of the request</param>
/// <param name="ct">Cancellation token to cancel waiting</param>
/// <returns>The time in milliseconds spend waiting</returns>
Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
}
}

View File

@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="uri"></param>
/// <param name="requestId"></param>
/// <returns></returns>
IRequest Create(HttpMethod method, string uri, int requestId);
IRequest Create(HttpMethod method, Uri uri, int requestId);
/// <summary>
/// Configure the requests created by this factory

View File

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiter;
namespace CryptoExchange.Net.Interfaces
{
@ -18,51 +15,19 @@ namespace CryptoExchange.Net.Interfaces
IRequestFactory RequestFactory { get; set; }
/// <summary>
/// What should happen when hitting a rate limit
/// </summary>
RateLimitingBehaviour RateLimitBehaviour { get; }
/// <summary>
/// List of active rate limiters
/// </summary>
IEnumerable<IRateLimiter> RateLimiters { get; }
/// <summary>
/// The total amount of requests made
/// The total amount of requests made with this client
/// </summary>
int TotalRequestsMade { get; }
/// <summary>
/// The base address of the API
/// The options provided for this client
/// </summary>
string BaseAddress { get; }
BaseRestClientOptions ClientOptions { get; }
/// <summary>
/// Client name
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
string ExchangeName { get; }
/// <summary>
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.
/// </summary>
/// <param name="limiter">The limiter to add</param>
void AddRateLimiter(IRateLimiter limiter);
/// <summary>
/// Removes all rate limiters from this client
/// </summary>
void RemoveRateLimiters();
/// <summary>
/// Ping to see if the server is reachable
/// </summary>
/// <returns>The roundtrip time of the ping request</returns>
CallResult<long> Ping(CancellationToken ct = default);
/// <summary>
/// Ping to see if the server is reachable
/// </summary>
/// <returns>The roundtrip time of the ping request</returns>
Task<CallResult<long>> PingAsync(CancellationToken ct = default);
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(ApiCredentials credentials);
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
@ -11,48 +12,37 @@ namespace CryptoExchange.Net.Interfaces
public interface ISocketClient: IDisposable
{
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// The options provided for this client
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
BaseSocketClientOptions ClientOptions { get; }
/// <summary>
/// The time in between reconnect attempts
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
TimeSpan ReconnectInterval { get; }
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(ApiCredentials credentials);
/// <summary>
/// Incoming kilobytes per second of data
/// </summary>
public double IncomingKbps { get; }
/// <summary>
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
/// </summary>
public int CurrentConnections { get; }
/// <summary>
/// Whether the client should try to auto reconnect when losing connection
/// The current amount of subscriptions running from the client
/// </summary>
bool AutoReconnect { get; }
public int CurrentSubscriptions { get; }
/// <summary>
/// The base address of the API
/// Unsubscribe from a stream using the subscription id received when starting the subscription
/// </summary>
string BaseAddress { get; }
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
TimeSpan ResponseTimeout { get; }
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
TimeSpan SocketNoDataTimeout { get; }
/// <summary>
/// The max amount of concurrent socket connections
/// </summary>
int MaxSocketConnections { get; }
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
int SocketCombineTarget { get; }
/// <inheritdoc cref="SocketClientOptions.MaxReconnectTries"/>
int? MaxReconnectTries { get; }
/// <inheritdoc cref="SocketClientOptions.MaxResubscribeTries"/>
int? MaxResubscribeTries { get; }
/// <inheritdoc cref="SocketClientOptions.MaxConcurrentResubscriptionsPerSocket"/>
int MaxConcurrentResubscriptionsPerSocket { get; }
/// <summary>
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
/// </summary>
double IncomingKbps { get; }
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(int subscriptionId);
/// <summary>
/// Unsubscribe from a stream

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
@ -10,6 +11,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public interface ISymbolOrderBook
{
/// <summary>
/// Identifier
/// </summary>
string Id { get; }
/// <summary>
/// The status of the order book. Order book is up to date when the status is `Synced`
/// </summary>
@ -39,7 +45,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Timestamp of the last update
/// </summary>
DateTime LastOrderBookUpdate { get; }
DateTime UpdateTime { get; }
/// <summary>
/// The number of asks in the book
@ -83,8 +89,9 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
/// <returns></returns>
Task<CallResult<bool>> StartAsync();
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
/// <summary>
/// Stop syncing the order book

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Security.Authentication;
using System.Text;
@ -7,80 +8,60 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Interface for websocket interaction
/// Webscoket connection interface
/// </summary>
public interface IWebsocket: IDisposable
{
/// <summary>
/// Websocket closed
/// Websocket closed event
/// </summary>
event Action OnClose;
/// <summary>
/// Websocket message received
/// Websocket message received event
/// </summary>
event Action<string> OnMessage;
/// <summary>
/// Websocket error
/// Websocket error event
/// </summary>
event Action<Exception> OnError;
/// <summary>
/// Websocket opened
/// Websocket opened event
/// </summary>
event Action OnOpen;
/// <summary>
/// Websocket has lost connection to the server and is attempting to reconnect
/// </summary>
event Action OnReconnecting;
/// <summary>
/// Websocket has reconnected to the server
/// </summary>
event Action OnReconnected;
/// <summary>
/// Get reconntion url
/// </summary>
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
/// <summary>
/// Id
/// Unique id for this socket
/// </summary>
int Id { get; }
/// <summary>
/// Origin
/// </summary>
string? Origin { get; set; }
/// <summary>
/// Encoding to use
/// </summary>
Encoding? Encoding { get; set; }
/// <summary>
/// Reconnecting
/// </summary>
bool Reconnecting { get; set; }
/// <summary>
/// The max amount of outgoing messages per second
/// </summary>
int? RatelimitPerSecond { get; set; }
/// <summary>
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
/// </summary>
double IncomingKbps { get; }
/// <summary>
/// Handler for byte data
/// The uri the socket connects to
/// </summary>
Func<byte[], string>? DataInterpreterBytes { get; set; }
Uri Uri { get; }
/// <summary>
/// Handler for string data
/// </summary>
Func<string, string>? DataInterpreterString { get; set; }
/// <summary>
/// Socket url
/// </summary>
string Url { get; }
/// <summary>
/// Is closed
/// Whether the socket connection is closed
/// </summary>
bool IsClosed { get; }
/// <summary>
/// Is open
/// Whether the socket connection is open
/// </summary>
bool IsOpen { get; }
/// <summary>
/// Supported ssl protocols
/// </summary>
SslProtocols SSLProtocols { get; set; }
/// <summary>
/// Timeout
/// </summary>
TimeSpan Timeout { get; set; }
/// <summary>
/// Connect the socket
/// </summary>
/// <returns></returns>
@ -91,18 +72,14 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="data"></param>
void Send(string data);
/// <summary>
/// Reset socket
/// Reconnect the socket
/// </summary>
void Reset();
/// <returns></returns>
Task ReconnectAsync();
/// <summary>
/// Close the connecting
/// Close the connection
/// </summary>
/// <returns></returns>
Task CloseAsync();
/// <summary>
/// Set proxy
/// </summary>
/// <param name="proxy"></param>
void SetProxy(ApiProxy proxy);
}
}

View File

@ -1,5 +1,5 @@
using System.Collections.Generic;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Sockets;
namespace CryptoExchange.Net.Interfaces
{
@ -11,18 +11,9 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Create a websocket for an url
/// </summary>
/// <param name="log"></param>
/// <param name="url"></param>
/// <param name="log">The logger</param>
/// <param name="parameters">The parameters to use for the connection</param>
/// <returns></returns>
IWebsocket CreateWebsocket(Log log, string url);
/// <summary>
/// Create a websocket for an url
/// </summary>
/// <param name="log"></param>
/// <param name="url"></param>
/// <param name="cookies"></param>
/// <param name="headers"></param>
/// <returns></returns>
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
}
}

View File

@ -4,7 +4,7 @@ using System;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Log to console
/// ILogger implementation for logging to the console
/// </summary>
public class ConsoleLogger : ILogger
{
@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Logging
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
Console.WriteLine(logMessage);

View File

@ -5,7 +5,7 @@ using System.Diagnostics;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Default log writer, writes to debug
/// Default log writer, uses Trace.WriteLine
/// </summary>
public class DebugLogger: ILogger
{
@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Logging
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
Trace.WriteLine(logMessage);

View File

@ -26,6 +26,8 @@ namespace CryptoExchange.Net.Logging
/// </summary>
public string ClientName { get; set; }
private readonly object _lock = new object();
/// <summary>
/// ctor
/// </summary>
@ -42,7 +44,8 @@ namespace CryptoExchange.Net.Logging
/// <param name="textWriters"></param>
public void UpdateWriters(List<ILogger> textWriters)
{
writers = textWriters;
lock (_lock)
writers = textWriters;
}
/// <summary>
@ -56,16 +59,19 @@ namespace CryptoExchange.Net.Logging
return;
var logMessage = $"{ClientName,-10} | {message}";
foreach (var writer in writers.ToList())
lock (_lock)
{
try
foreach (var writer in writers)
{
writer.Log(logLevel, logMessage);
}
catch (Exception e)
{
// Can't write to the logging so where else to output..
Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
try
{
writer.Log(logLevel, logMessage);
}
catch (Exception e)
{
// Can't write to the logging so where else to output..
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
}
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
@ -12,10 +11,10 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public class AsyncResetEvent : IDisposable
{
private readonly static Task<bool> _completed = Task.FromResult(true);
private static readonly Task<bool> _completed = Task.FromResult(true);
private readonly Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
private bool _signaled;
private bool _reset;
private readonly bool _reset;
/// <summary>
/// New AsyncResetEvent

View File

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
{
@ -36,16 +38,6 @@ namespace CryptoExchange.Net.Objects
{
return obj?.Success == true;
}
/// <summary>
/// Create an error result
/// </summary>
/// <param name="error"></param>
/// <returns></returns>
public static WebCallResult CreateErrorResult(Error error)
{
return new WebCallResult(null, null, error);
}
}
/// <summary>
@ -62,22 +54,36 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; set; }
public string? OriginalData { get; internal set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="data"></param>
/// <param name="originalData"></param>
/// <param name="error"></param>
#pragma warning disable 8618
public CallResult([AllowNull]T data, Error? error): base(error)
protected CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
#pragma warning restore 8618
{
OriginalData = originalData;
#pragma warning disable 8601
Data = data;
#pragma warning restore 8601
}
/// <summary>
/// Create a new data result
/// </summary>
/// <param name="data">The data to return</param>
public CallResult(T data) : this(data, null, null) { }
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The erro rto return</param>
public CallResult(Error error) : this(default, null, error) { }
/// <summary>
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
/// </summary>
@ -111,16 +117,6 @@ namespace CryptoExchange.Net.Objects
}
}
/// <summary>
/// Create an error result
/// </summary>
/// <param name="error"></param>
/// <returns></returns>
public new static WebCallResult<T> CreateErrorResult(Error error)
{
return new WebCallResult<T>(null, null, default, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
@ -129,7 +125,18 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public CallResult<K> As<K>([AllowNull] K data)
{
return new CallResult<K>(data, Error);
return new CallResult<K>(data, OriginalData, Error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error to return</param>
/// <returns></returns>
public CallResult<K> AsError<K>(Error error)
{
return new CallResult<K>(default, OriginalData, error);
}
}
@ -138,6 +145,26 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public class WebCallResult : CallResult
{
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; set; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
@ -148,40 +175,56 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="code">Status code</param>
/// <param name="responseHeaders">Response headers</param>
/// <param name="error">Error</param>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error? error) : base(error)
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
TimeSpan? responseTime,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
Error? error) : base(error)
{
ResponseHeaders = responseHeaders;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
}
/// <summary>
/// Create an error result
/// ctor
/// </summary>
/// <param name="code">Status code</param>
/// <param name="responseHeaders">Response headers</param>
/// <param name="error">Error</param>
/// <returns></returns>
public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
{
return new WebCallResult(code, responseHeaders, error);
}
/// <param name="error"></param>
public WebCallResult(Error error): base(error) { }
/// <summary>
/// Create an error result
/// Return the result as an error result
/// </summary>
/// <param name="result"></param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public static WebCallResult CreateErrorResult(WebCallResult result)
public WebCallResult AsError(Error error)
{
return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, result.Error);
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
}
@ -191,6 +234,26 @@ namespace CryptoExchange.Net.Objects
/// <typeparam name="T"></typeparam>
public class WebCallResult<T>: CallResult<T>
{
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; set; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
@ -200,44 +263,53 @@ namespace CryptoExchange.Net.Objects
/// The response headers
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
[AllowNull] T data,
Error? error): base(data, error)
{
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
}
/// <summary>
/// ctor
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// Create a new result
/// </summary>
/// <param name="code"></param>
/// <param name="originalData"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="originalData"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
TimeSpan? responseTime,
string? originalData,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
[AllowNull] T data,
Error? error) : base(data, error)
Error? error) : base(data, originalData, error)
{
OriginalData = originalData;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
}
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
@ -246,19 +318,36 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, OriginalData, data, Error);
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
}
/// <summary>
/// Create an error result
/// Copy as a dataless result
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="error"></param>
/// <returns></returns>
public static WebCallResult<T> CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
public WebCallResult AsDataless()
{
return new WebCallResult<T>(code, responseHeaders, default, error);
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error returned</param>
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
}
}
}

View File

@ -69,7 +69,15 @@
/// <summary>
/// Data synced, order book is up to date
/// </summary>
Synced
Synced,
/// <summary>
/// Disposing
/// </summary>
Disposing,
/// <summary>
/// Disposed
/// </summary>
Disposed
}
/// <summary>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
@ -9,25 +10,66 @@ using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Base options
/// Base options, applicable to everything
/// </summary>
public class BaseOptions
{
/// <summary>
/// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers.
/// </summary>
public LogLevel? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information;
internal event Action? OnLoggingChanged;
private LogLevel _logLevel = LogLevel.Information;
/// <summary>
/// The minimum log level to output
/// </summary>
public LogLevel LogLevel
{
get => _logLevel;
set
{
_logLevel = value;
OnLoggingChanged?.Invoke();
}
}
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
/// <summary>
/// The log writers
/// </summary>
public List<ILogger> LogWriters { get; set; } = new List<ILogger> { new DebugLogger() };
public List<ILogger> LogWriters
{
get => _logWriters;
set
{
_logWriters = value;
OnLoggingChanged?.Invoke();
}
}
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
/// </summary>
public bool OutputOriginalData { get; set; } = false;
/// <summary>
/// ctor
/// </summary>
public BaseOptions(): this(null)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOptions">Copy options from these options to the new options</param>
public BaseOptions(BaseOptions? baseOptions)
{
if (baseOptions == null)
return;
LogLevel = baseOptions.LogLevel;
LogWriters = baseOptions.LogWriters.ToList();
OutputOriginalData = baseOptions.OutputOriginalData;
}
/// <inheritdoc />
public override string ToString()
{
@ -36,184 +78,93 @@ namespace CryptoExchange.Net.Objects
}
/// <summary>
/// Base for order book options
/// Client options, for both the socket and rest clients
/// </summary>
public class OrderBookOptions : BaseOptions
{
/// <summary>
/// The name of the order book implementation
/// </summary>
public string OrderBookName { get; }
/// <summary>
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
/// </summary>
public bool ChecksumValidationEnabled { get; set; } = true;
/// <summary>
/// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.
/// </summary>
public bool SequenceNumbersAreConsecutive { get; }
/// <summary>
/// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
/// when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed
/// </summary>
public bool StrictLevels { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="name">The name of the order book implementation</param>
/// <param name="sequencesAreConsecutive">Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.</param>
/// <param name="strictLevels">Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
/// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed</param>
public OrderBookOptions(string name, bool sequencesAreConsecutive, bool strictLevels)
{
OrderBookName = name;
SequenceNumbersAreConsecutive = sequencesAreConsecutive;
StrictLevels = strictLevels;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}, StrictLevels: {StrictLevels}";
}
}
/// <summary>
/// Base client options
/// </summary>
public class ClientOptions : BaseOptions
public class BaseClientOptions : BaseOptions
{
private string _baseAddress;
/// <summary>
/// The base address of the client
/// </summary>
public string BaseAddress
{
get => _baseAddress;
set
{
var newValue = value;
if (!newValue.EndsWith("/"))
newValue += "/";
_baseAddress = newValue;
}
}
/// <summary>
/// The api credentials
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Should check objects for missing properties based on the model and the received JSON
/// </summary>
public bool ShouldCheckObjects { get; set; } = false;
/// <summary>
/// Proxy to use
/// Proxy to use when connecting
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials to be used for signing requests to private endpoints. These credentials will be used for each API in the client, unless overriden in the API options
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">The base address to use</param>
#pragma warning disable 8618
public ClientOptions(string baseAddress)
#pragma warning restore 8618
public BaseClientOptions() : this(null)
{
BaseAddress = baseAddress;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOptions">Copy options from these options to the new options</param>
public BaseClientOptions(BaseClientOptions? baseOptions) : base(baseOptions)
{
if (baseOptions == null)
return;
Proxy = baseOptions.Proxy;
ApiCredentials = baseOptions.ApiCredentials?.Copy();
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
return $"{base.ToString()}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}, Base.ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
}
/// <summary>
/// Base for rest client options
/// Rest client options
/// </summary>
public class RestClientOptions : ClientOptions
public class BaseRestClientOptions : BaseClientOptions
{
/// <summary>
/// List of rate limiters to use
/// </summary>
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
/// <summary>
/// What to do when a call would exceed the rate limit
/// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
/// <summary>
/// The time the server has to respond to a request before timing out
/// </summary>
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored in requests and should be set on the provided HttpClient instance
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
/// </summary>
public HttpClient? HttpClient { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">The base address of the API</param>
public RestClientOptions(string baseAddress): base(baseAddress)
public BaseRestClientOptions(): this(null)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">The base address of the API</param>
/// <param name="httpClient">Shared http client instance</param>
public RestClientOptions(HttpClient httpClient, string baseAddress) : base(baseAddress)
/// <param name="baseOptions">Copy options from these options to the new options</param>
public BaseRestClientOptions(BaseRestClientOptions? baseOptions): base(baseOptions)
{
HttpClient = httpClient;
}
/// <summary>
/// Create a copy of the options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : RestClientOptions, new()
{
var copy = new T
{
BaseAddress = BaseAddress,
LogLevel = LogLevel,
Proxy = Proxy,
LogWriters = LogWriters,
RateLimiters = RateLimiters,
RateLimitingBehaviour = RateLimitingBehaviour,
RequestTimeout = RequestTimeout,
HttpClient = HttpClient
};
if (baseOptions == null)
return;
if (ApiCredentials != null)
copy.ApiCredentials = ApiCredentials.Copy();
return copy;
HttpClient = baseOptions.HttpClient;
RequestTimeout = baseOptions.RequestTimeout;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}";
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}";
}
}
/// <summary>
/// Base for socket client options
/// Socket client options
/// </summary>
public class SocketClientOptions : ClientOptions
public class BaseSocketClientOptions : BaseClientOptions
{
/// <summary>
/// Whether or not the socket should automatically reconnect when losing connection
@ -225,74 +176,186 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// The maximum number of times to try to reconnect
/// </summary>
public int? MaxReconnectTries { get; set; }
/// <summary>
/// The maximum number of times to try to resubscribe after reconnecting
/// </summary>
public int? MaxResubscribeTries { get; set; } = 5;
/// <summary>
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
/// </summary>
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
/// <summary>
/// The time to wait for a socket response before giving a timeout
/// The max time to wait for a response after sending a request on the socket before giving a timeout
/// </summary>
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// The time after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected.
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
/// for example when the server sends intermittent ping requests
/// </summary>
public TimeSpan SocketNoDataTimeout { get; set; }
/// <summary>
/// The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket.
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
/// </summary>
public int? SocketSubscriptionsCombineTarget { get; set; }
/// <summary>
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
/// </summary>
public int? MaxSocketConnections { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">The base address to use</param>
public SocketClientOptions(string baseAddress) : base(baseAddress)
public BaseSocketClientOptions(): this(null)
{
}
/// <summary>
/// Create a copy of the options
/// ctor
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketClientOptions, new()
/// <param name="baseOptions">Copy options from these options to the new options</param>
public BaseSocketClientOptions(BaseSocketClientOptions? baseOptions): base(baseOptions)
{
var copy = new T
{
BaseAddress = BaseAddress,
LogLevel = LogLevel,
Proxy = Proxy,
LogWriters = LogWriters,
AutoReconnect = AutoReconnect,
ReconnectInterval = ReconnectInterval,
SocketResponseTimeout = SocketResponseTimeout,
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget
};
if (baseOptions == null)
return;
if (ApiCredentials != null)
copy.ApiCredentials = ApiCredentials.Copy();
return copy;
AutoReconnect = baseOptions.AutoReconnect;
ReconnectInterval = baseOptions.ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
MaxSocketConnections = baseOptions.MaxSocketConnections;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
}
}
/// <summary>
/// API client options
/// </summary>
public class ApiClientOptions
{
/// <summary>
/// The base address of the API
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// ctor
/// </summary>
#pragma warning disable 8618 // Will always get filled by the implementation
public ApiClientOptions()
{
}
#pragma warning restore 8618
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">Base address for the API</param>
public ApiClientOptions(string baseAddress)
{
BaseAddress = baseAddress;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOptions">Copy values for the provided options</param>
/// <param name="newValues">Copy values for the provided options</param>
public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
{
BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
}
/// <inheritdoc />
public override string ToString()
{
return $"Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
}
}
/// <summary>
/// Rest API client options
/// </summary>
public class RestApiClientOptions: ApiClientOptions
{
/// <summary>
/// List of rate limiters to use
/// </summary>
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
/// <summary>
/// What to do when a call would exceed the rate limit
/// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool AutoTimestamp { get; set; }
/// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary>
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// ctor
/// </summary>
public RestApiClientOptions()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress">Base address for the API</param>
public RestApiClientOptions(string baseAddress): base(baseAddress)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="baseOn">Copy values for the provided options</param>
/// <param name="newValues">Copy values for the provided options</param>
public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
{
RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
}
}
/// <summary>
/// Base for order book options
/// </summary>
public class OrderBookOptions : BaseOptions
{
/// <summary>
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
/// </summary>
public bool ChecksumValidationEnabled { get; set; } = true;
}
}

View File

@ -0,0 +1,408 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Limits the amount of requests to a certain constraint
/// </summary>
public class RateLimiter : IRateLimiter
{
private readonly object _limiterLock = new object();
internal List<Limiter> Limiters = new List<Limiter>();
/// <summary>
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
/// <see cref="AddEndpointLimit(string, int, TimeSpan, HttpMethod?, bool)"/>, <see cref="AddPartialEndpointLimit(string, int, TimeSpan, HttpMethod?, bool, bool)"/> or <see cref="AddApiKeyLimit"/>.
/// </summary>
public RateLimiter()
{
}
/// <summary>
/// Add a rate limit for the total amount of requests per time period
/// </summary>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
{
lock(_limiterLock)
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
return this;
}
/// <summary>
/// Add a rate lmit for the amount of requests per time for an endpoint
/// </summary>
/// <param name="endpoint">The endpoint the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="method">The HttpMethod the limit is for, null for all</param>
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{
lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
/// <summary>
/// Add a rate lmit for the amount of requests per time for an endpoint
/// </summary>
/// <param name="endpoints">The endpoints the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="method">The HttpMethod the limit is for, null for all</param>
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{
lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
/// <summary>
/// Add a rate lmit for the amount of requests per time for an endpoint
/// </summary>
/// <param name="endpoint">The endpoint the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="method">The HttpMethod the limit is for, null for all</param>
/// <param name="ignoreOtherRateLimits">If set to true it ignores other rate limits</param>
/// <param name="countPerEndpoint">Whether all requests for this partial endpoint are bound to the same limit or each individual endpoint has its own limit</param>
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
{
lock(_limiterLock)
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
return this;
}
/// <summary>
/// Add a rate limit for the amount of requests per Api key
/// </summary>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="onlyForSignedRequests">Only include calls that are signed in this limiter</param>
/// <param name="excludeFromTotalRateLimit">Exclude requests with API key from the total rate limiter</param>
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
{
lock(_limiterLock)
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
return this;
}
/// <inheritdoc />
public async Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
{
int totalWaitTime = 0;
EndpointRateLimiter? endpointLimit;
lock (_limiterLock)
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
if(endpointLimit != null)
{
var waitResult = await ProcessTopic(log, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
if (endpointLimit?.IgnoreOtherRateLimits == true)
return new CallResult<int>(totalWaitTime);
List<PartialEndpointRateLimiter> partialEndpointLimits;
lock (_limiterLock)
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
foreach (var partialEndpointLimit in partialEndpointLimits)
{
if (partialEndpointLimit.CountPerEndpoint)
{
SingleTopicRateLimiter? thisEndpointLimit;
lock (_limiterLock)
{
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
if (thisEndpointLimit == null)
{
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
Limiters.Add(thisEndpointLimit);
}
}
var waitResult = await ProcessTopic(log, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
else
{
var waitResult = await ProcessTopic(log, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
return new CallResult<int>(totalWaitTime);
ApiKeyRateLimiter? apiLimit;
lock (_limiterLock)
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
if (apiLimit != null)
{
if(apiKey == null)
{
if (!apiLimit.OnlyForSignedRequests)
{
var waitResult = await ProcessTopic(log, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
else if (signed || !apiLimit.OnlyForSignedRequests)
{
SingleTopicRateLimiter? thisApiLimit;
lock (_limiterLock)
{
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
if (thisApiLimit == null)
{
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
Limiters.Add(thisApiLimit);
}
}
var waitResult = await ProcessTopic(log, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
return new CallResult<int>(totalWaitTime);
TotalRateLimiter? totalLimit;
lock (_limiterLock)
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
if (totalLimit != null)
{
var waitResult = await ProcessTopic(log, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
return new CallResult<int>(totalWaitTime);
}
private static async Task<CallResult<int>> ProcessTopic(Log log, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
try
{
await historyTopic.Semaphore.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult<int>(new CancellationRequestedError());
}
sw.Stop();
int totalWaitTime = 0;
while (true)
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
for (var i = 0; i < historyTopic.Entries.Count; i++)
{
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
{
historyTopic.Entries.Remove(historyTopic.Entries[i]);
i--;
}
else
break;
}
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
if (currentWeight + requestWeight > historyTopic.Limit)
{
if (currentWeight == 0)
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
// Wait until the next entry should be removed from the history
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
if (thisWaitTime > 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
{
historyTopic.Semaphore.Release();
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
log.Write(LogLevel.Warning, msg);
return new CallResult<int>(new RateLimitError(msg));
}
log.Write(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
try
{
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult<int>(new CancellationRequestedError());
}
totalWaitTime += thisWaitTime;
}
}
else
{
break;
}
}
var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
historyTopic.Semaphore.Release();
return new CallResult<int>(totalWaitTime);
}
internal struct LimitEntry
{
public DateTime Timestamp { get; set; }
public int Weight { get; set; }
public LimitEntry(DateTime timestamp, int weight)
{
Timestamp = timestamp;
Weight = weight;
}
}
internal class Limiter
{
public RateLimitType Type { get; set; }
public HttpMethod? Method { get; set; }
public SemaphoreSlim Semaphore { get; set; }
public int Limit { get; set; }
public TimeSpan Period { get; set; }
public List<LimitEntry> Entries { get; set; } = new List<LimitEntry>();
public Limiter(RateLimitType type, int limit, TimeSpan perPeriod, HttpMethod? method)
{
Semaphore = new SemaphoreSlim(1, 1);
Type = type;
Limit = limit;
Period = perPeriod;
Method = method;
}
}
internal class TotalRateLimiter : Limiter
{
public TotalRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method)
: base(RateLimitType.Total, limit, perPeriod, method)
{
}
public override string ToString()
{
return nameof(TotalRateLimiter);
}
}
internal class EndpointRateLimiter: Limiter
{
public string[] Endpoints { get; set; }
public bool IgnoreOtherRateLimits { get; set; }
public EndpointRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits)
:base(RateLimitType.Endpoint, limit, perPeriod, method)
{
Endpoints = endpoints;
IgnoreOtherRateLimits = ignoreOtherRateLimits;
}
public override string ToString()
{
return nameof(EndpointRateLimiter) + $": {string.Join(", ", Endpoints)}";
}
}
internal class PartialEndpointRateLimiter : Limiter
{
public string[] PartialEndpoints { get; set; }
public bool IgnoreOtherRateLimits { get; set; }
public bool CountPerEndpoint { get; set; }
public PartialEndpointRateLimiter(string[] partialEndpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits, bool countPerEndpoint)
: base(RateLimitType.PartialEndpoint, limit, perPeriod, method)
{
PartialEndpoints = partialEndpoints;
IgnoreOtherRateLimits = ignoreOtherRateLimits;
CountPerEndpoint = countPerEndpoint;
}
public override string ToString()
{
return nameof(PartialEndpointRateLimiter) + $": {string.Join(", ", PartialEndpoints)}";
}
}
internal class ApiKeyRateLimiter : Limiter
{
public bool OnlyForSignedRequests { get; set; }
public bool IgnoreTotalRateLimit { get; set; }
public ApiKeyRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method, bool onlyForSignedRequests, bool ignoreTotalRateLimit)
:base(RateLimitType.ApiKey, limit, perPeriod, method)
{
OnlyForSignedRequests = onlyForSignedRequests;
IgnoreTotalRateLimit = ignoreTotalRateLimit;
}
}
internal class SingleTopicRateLimiter: Limiter
{
public object Topic { get; set; }
public SingleTopicRateLimiter(object topic, Limiter limiter)
:base(limiter.Type, limiter.Limit, limiter.Period, limiter.Method)
{
Topic = topic;
}
public override string ToString()
{
return (Type == RateLimitType.ApiKey ? nameof(ApiKeyRateLimiter): nameof(EndpointRateLimiter)) + $": {Topic}";
}
}
internal enum RateLimitType
{
Total,
Endpoint,
PartialEndpoint,
ApiKey
}
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Threading;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// The time synchronization state of an API client
/// </summary>
public class TimeSyncState
{
/// <summary>
/// Name of the API
/// </summary>
public string ApiName { get; set; }
/// <summary>
/// Semaphore to use for checking the time syncing. Should be shared instance among the API client
/// </summary>
public SemaphoreSlim Semaphore { get; }
/// <summary>
/// Last sync time for the API client
/// </summary>
public DateTime LastSyncTime { get; set; }
/// <summary>
/// Time offset for the API client
/// </summary>
public TimeSpan TimeOffset { get; set; }
/// <summary>
/// ctor
/// </summary>
public TimeSyncState(string apiName)
{
ApiName = apiName;
Semaphore = new SemaphoreSlim(1, 1);
}
}
/// <summary>
/// Time synchronization info
/// </summary>
public class TimeSyncInfo
{
/// <summary>
/// Logger
/// </summary>
public Log Log { get; }
/// <summary>
/// Should synchronize time
/// </summary>
public bool SyncTime { get; }
/// <summary>
/// Timestamp recalulcation interval
/// </summary>
public TimeSpan RecalculationInterval { get; }
/// <summary>
/// Time sync state for the API client
/// </summary>
public TimeSyncState TimeSyncState { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="log"></param>
/// <param name="recalculationInterval"></param>
/// <param name="syncTime"></param>
/// <param name="syncState"></param>
public TimeSyncInfo(Log log, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
{
Log = log;
SyncTime = syncTime;
RecalculationInterval = recalculationInterval;
TimeSyncState = syncState;
}
/// <summary>
/// Set the time offset
/// </summary>
/// <param name="offset"></param>
public void UpdateTimeOffset(TimeSpan offset)
{
TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
TimeSyncState.TimeOffset = TimeSpan.Zero;
}
else
{
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
TimeSyncState.TimeOffset = offset;
}
}
}
}

View File

@ -10,19 +10,19 @@ namespace CryptoExchange.Net.OrderBook
public class ProcessBufferRangeSequenceEntry
{
/// <summary>
/// First update id
/// First sequence number in this update
/// </summary>
public long FirstUpdateId { get; set; }
/// <summary>
/// Last update id
/// Last sequence number in this update
/// </summary>
public long LastUpdateId { get; set; }
/// <summary>
/// List of asks
/// List of changed/new asks
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
/// <summary>
/// List of bids
/// List of changed/new bids
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.RateLimiter
{
/// <summary>
/// Rate limiting object
/// </summary>
public class RateLimitObject
{
/// <summary>
/// Lock
/// </summary>
public object LockObject { get; }
private List<DateTime> Times { get; }
/// <summary>
/// ctor
/// </summary>
public RateLimitObject()
{
LockObject = new object();
Times = new List<DateTime>();
}
/// <summary>
/// Get time to wait for a specific time
/// </summary>
/// <param name="time"></param>
/// <param name="limit"></param>
/// <param name="perTimePeriod"></param>
/// <returns></returns>
public int GetWaitTime(DateTime time, int limit, TimeSpan perTimePeriod)
{
Times.RemoveAll(d => d < time - perTimePeriod);
if (Times.Count >= limit)
return (int)Math.Round((Times.First() - (time - perTimePeriod)).TotalMilliseconds);
return 0;
}
/// <summary>
/// Add an executed request time
/// </summary>
/// <param name="time"></param>
public void Add(DateTime time)
{
Times.Add(time);
Times.Sort();
}
}
}

View File

@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.RateLimiter
{
/// <summary>
/// Limits the amount of requests per time period to a certain limit, counts the request per API key.
/// </summary>
public class RateLimiterAPIKey: IRateLimiter, IDisposable
{
internal Dictionary<string, RateLimitObject> history = new Dictionary<string, RateLimitObject>();
private readonly SHA256 encryptor;
private readonly int limitPerKey;
private readonly TimeSpan perTimePeriod;
private readonly object historyLock = new object();
/// <summary>
/// Create a new RateLimiterAPIKey. This rate limiter limits the amount of requests per time period to a certain limit, counts the request per API key.
/// </summary>
/// <param name="limitPerApiKey">The amount to limit to</param>
/// <param name="perTimePeriod">The time period over which the limit counts</param>
public RateLimiterAPIKey(int limitPerApiKey, TimeSpan perTimePeriod)
{
limitPerKey = limitPerApiKey;
encryptor = SHA256.Create();
this.perTimePeriod = perTimePeriod;
}
/// <inheritdoc />
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits = 1)
{
if(client.authProvider?.Credentials?.Key == null)
return new CallResult<double>(0, null);
var keyBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(client.authProvider.Credentials.Key.GetString()));
StringBuilder builder = new StringBuilder();
for (int i = 0; i < keyBytes.Length; i++)
{
builder.Append(keyBytes[i].ToString("x2"));
}
var key = builder.ToString();
int waitTime;
RateLimitObject rlo;
lock (historyLock)
{
if (history.ContainsKey(key))
rlo = history[key];
else
{
rlo = new RateLimitObject();
history.Add(key, rlo);
}
}
var sw = Stopwatch.StartNew();
lock (rlo.LockObject)
{
sw.Stop();
waitTime = rlo.GetWaitTime(DateTime.UtcNow, limitPerKey, perTimePeriod);
if (waitTime != 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
return new CallResult<double>(waitTime, new RateLimitError($"endpoint limit of {limitPerKey} reached on api key " + key));
Thread.Sleep(Convert.ToInt32(waitTime));
waitTime += (int)sw.ElapsedMilliseconds;
}
rlo.Add(DateTime.UtcNow);
}
return new CallResult<double>(waitTime, null);
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
encryptor.Dispose();
}
}
}

View File

@ -1,65 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace CryptoExchange.Net.RateLimiter
{
/// <summary>
/// Limits the amount of requests per time period to a certain limit, counts the total amount of requests.
/// </summary>
public class RateLimiterCredit : IRateLimiter
{
internal List<DateTime> history = new List<DateTime>();
private readonly int limit;
private readonly TimeSpan perTimePeriod;
private readonly object requestLock = new object();
/// <summary>
/// Create a new RateLimiterTotal. This rate limiter limits the amount of requests per time period to a certain limit, counts the total amount of requests.
/// </summary>
/// <param name="limit">The amount to limit to</param>
/// <param name="perTimePeriod">The time period over which the limit counts</param>
public RateLimiterCredit(int limit, TimeSpan perTimePeriod)
{
this.limit = limit;
this.perTimePeriod = perTimePeriod;
}
/// <inheritdoc />
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits = 1)
{
var sw = Stopwatch.StartNew();
lock (requestLock)
{
sw.Stop();
double waitTime = 0;
var checkTime = DateTime.UtcNow;
history.RemoveAll(d => d < checkTime - perTimePeriod);
if (history.Count >= limit)
{
waitTime = (history.First() - (checkTime - perTimePeriod)).TotalMilliseconds;
if (waitTime > 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
return new CallResult<double>(waitTime, new RateLimitError($"total limit of {limit} reached"));
Thread.Sleep(Convert.ToInt32(waitTime));
waitTime += sw.ElapsedMilliseconds;
}
}
for (int i = 1; i <= credits; i++)
history.Add(DateTime.UtcNow);
history.Sort();
return new CallResult<double>(waitTime, null);
}
}
}
}

View File

@ -1,68 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace CryptoExchange.Net.RateLimiter
{
/// <summary>
/// Limits the amount of requests per time period to a certain limit, counts the request per endpoint.
/// </summary>
public class RateLimiterPerEndpoint: IRateLimiter
{
internal Dictionary<string, RateLimitObject> history = new Dictionary<string, RateLimitObject>();
private readonly int limitPerEndpoint;
private readonly TimeSpan perTimePeriod;
private readonly object historyLock = new object();
/// <summary>
/// Create a new RateLimiterPerEndpoint. This rate limiter limits the amount of requests per time period to a certain limit, counts the request per endpoint.
/// </summary>
/// <param name="limitPerEndpoint">The amount to limit to</param>
/// <param name="perTimePeriod">The time period over which the limit counts</param>
public RateLimiterPerEndpoint(int limitPerEndpoint, TimeSpan perTimePeriod)
{
this.limitPerEndpoint = limitPerEndpoint;
this.perTimePeriod = perTimePeriod;
}
/// <inheritdoc />
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitingBehaviour, int credits = 1)
{
int waitTime;
RateLimitObject rlo;
lock (historyLock)
{
if (history.ContainsKey(url))
rlo = history[url];
else
{
rlo = new RateLimitObject();
history.Add(url, rlo);
}
}
var sw = Stopwatch.StartNew();
lock (rlo.LockObject)
{
sw.Stop();
waitTime = rlo.GetWaitTime(DateTime.UtcNow, limitPerEndpoint, perTimePeriod);
if (waitTime != 0)
{
if(limitingBehaviour == RateLimitingBehaviour.Fail)
return new CallResult<double>(waitTime, new RateLimitError($"endpoint limit of {limitPerEndpoint} reached on endpoint " + url));
Thread.Sleep(Convert.ToInt32(waitTime));
waitTime += (int)sw.ElapsedMilliseconds;
}
rlo.Add(DateTime.UtcNow);
}
return new CallResult<double>(waitTime, null);
}
}
}

View File

@ -1,63 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace CryptoExchange.Net.RateLimiter
{
/// <summary>
/// Limits the amount of requests per time period to a certain limit, counts the total amount of requests.
/// </summary>
public class RateLimiterTotal: IRateLimiter
{
internal List<DateTime> history = new List<DateTime>();
private readonly int limit;
private readonly TimeSpan perTimePeriod;
private readonly object requestLock = new object();
/// <summary>
/// Create a new RateLimiterTotal. This rate limiter limits the amount of requests per time period to a certain limit, counts the total amount of requests.
/// </summary>
/// <param name="limit">The amount to limit to</param>
/// <param name="perTimePeriod">The time period over which the limit counts</param>
public RateLimiterTotal(int limit, TimeSpan perTimePeriod)
{
this.limit = limit;
this.perTimePeriod = perTimePeriod;
}
/// <inheritdoc />
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits = 1)
{
var sw = Stopwatch.StartNew();
lock (requestLock)
{
sw.Stop();
double waitTime = 0;
var checkTime = DateTime.UtcNow;
history.RemoveAll(d => d < checkTime - perTimePeriod);
if (history.Count >= limit)
{
waitTime = (history.First() - (checkTime - perTimePeriod)).TotalMilliseconds;
if (waitTime > 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
return new CallResult<double>(waitTime, new RateLimitError($"total limit of {limit} reached"));
Thread.Sleep(Convert.ToInt32(waitTime));
waitTime += sw.ElapsedMilliseconds;
}
}
history.Add(DateTime.UtcNow);
history.Sort();
return new CallResult<double>(waitTime, null);
}
}
}
}

View File

@ -11,7 +11,7 @@ using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Requests
{
/// <summary>
/// Request object
/// Request object, wrapper for HttpRequestMessage
/// </summary>
public class Request : IRequest
{
@ -49,6 +49,7 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public Uri Uri => request.RequestUri;
/// <inheritdoc />
public int RequestId { get; }

View File

@ -7,7 +7,7 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Requests
{
/// <summary>
/// WebRequest factory
/// Request factory
/// </summary>
public class RequestFactory : IRequestFactory
{
@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Requests
}
/// <inheritdoc />
public IRequest Create(HttpMethod method, string uri, int requestId)
public IRequest Create(HttpMethod method, Uri uri, int requestId)
{
if (httpClient == null)
throw new InvalidOperationException("Cant create request before configuring http client");

View File

@ -8,7 +8,7 @@ using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Requests
{
/// <summary>
/// HttpWebResponse response object
/// Response object, wrapper for HttpResponseMessage
/// </summary>
internal class Response : IResponse
{

View File

@ -1,456 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiter;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
/// <summary>
/// Base rest client
/// </summary>
public abstract class RestClient : BaseClient, IRestClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
protected 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 }
};
/// <summary>
/// Request body content type
/// </summary>
protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// Whether or not we need to manually parse an error instead of relying on the http status code
/// </summary>
protected bool manualParseError = false;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
protected 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 string requestBodyEmptyContent = "{}";
/// <summary>
/// Timeout for requests. This setting is ignored when injecting a HttpClient in the options, requests timeouts should be set on the client then.
/// </summary>
public TimeSpan RequestTimeout { get; }
/// <summary>
/// What should happen when running into a rate limit
/// </summary>
public RateLimitingBehaviour RateLimitBehaviour { get; }
/// <summary>
/// List of rate limiters
/// </summary>
public IEnumerable<IRateLimiter> RateLimiters { get; private set; }
/// <summary>
/// Total requests made by this client
/// </summary>
public int TotalRequestsMade { get; private set; }
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="exchangeName">The name of the exchange this client is for</param>
/// <param name="exchangeOptions">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 RestClient(string exchangeName, RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(exchangeName, exchangeOptions, authenticationProvider)
{
if (exchangeOptions == null)
throw new ArgumentNullException(nameof(exchangeOptions));
RequestTimeout = exchangeOptions.RequestTimeout;
RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy, exchangeOptions.HttpClient);
RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in exchangeOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
}
/// <summary>
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.
/// </summary>
/// <param name="limiter">The limiter to add</param>
public void AddRateLimiter(IRateLimiter limiter)
{
if (limiter == null)
throw new ArgumentNullException(nameof(limiter));
var rateLimiters = RateLimiters.ToList();
rateLimiters.Add(limiter);
RateLimiters = rateLimiters;
}
/// <summary>
/// Removes all rate limiters from this client
/// </summary>
public void RemoveRateLimiters()
{
RateLimiters = new List<IRateLimiter>();
}
/// <summary>
/// Ping to see if the server is reachable
/// </summary>
/// <returns>The roundtrip time of the ping request</returns>
public virtual CallResult<long> Ping(CancellationToken ct = default) => PingAsync(ct).Result;
/// <summary>
/// Ping to see if the server is reachable
/// </summary>
/// <returns>The roundtrip time of the ping request</returns>
public virtual async Task<CallResult<long>> PingAsync(CancellationToken ct = default)
{
var ping = new Ping();
var uri = new Uri(BaseAddress);
PingReply reply;
var ctRegistration = ct.Register(() => ping.SendAsyncCancel());
try
{
reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false);
}
catch (PingException e)
{
if (e.InnerException == null)
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + e.Message });
if (e.InnerException is SocketException exception)
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode });
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message });
}
finally
{
ctRegistration.Dispose();
ping.Dispose();
}
if (ct.IsCancellationRequested)
return new CallResult<long>(0, new CancellationRequestedError());
return reply.Status == IPStatus.Success ? new CallResult<long>(reply.RoundtripTime, null) : new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + reply.Status });
}
/// <summary>
/// Execute a request to the uri and deserialize the response into the provided type parameter
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="checkResult">Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="credits">Credits used for the request</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
bool checkResult = true,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int credits = 1,
JsonSerializer? deserializer = null,
Dictionary<string, string>? additionalHeaders = null) where T : class
{
var requestId = NextId();
log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri);
if (signed && authProvider == null)
{
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new WebCallResult<T>(null, null, null, new NoApiCredentialsError());
}
var paramsPosition = parameterPosition ?? ParameterPositions[method];
var request = ConstructRequest(uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
foreach (var limiter in RateLimiters)
{
var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour, credits);
if (!limitResult.Success)
{
log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} failed because of rate limit");
return new WebCallResult<T>(null, null, null, limitResult.Error);
}
if (limitResult.Data > 0)
log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}");
}
string? paramString = "";
if (paramsPosition == HttpMethodParameterPosition.InBody)
paramString = " with request body " + request.Content;
if (log.Level == LogLevel.Trace)
{
var headers = request.GetHeaders();
if (headers.Any())
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
}
log.Write(LogLevel.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")}");
return await GetResponseAsync<T>(request, deserializer, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
/// <param name="request">The request object to execute</param>
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken)
{
try
{
TotalRequestsMade++;
var sw = Stopwatch.StartNew();
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
if (manualParseError)
{
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Close();
response.Close();
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms: {data}");
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
var parseResult = ValidateJson(data);
if (!parseResult.Success)
return WebCallResult<T>.CreateErrorResult(response.StatusCode, response.ResponseHeaders, parseResult.Error!);
// Let the library implementation see if it is an error response, and if so parse the error
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
if (error != null)
return WebCallResult<T>.CreateErrorResult(response.StatusCode, response.ResponseHeaders, error);
// Not an error, so continue deserializing
var deserializeResult = Deserialize<T>(parseResult.Data, null, deserializer, request.RequestId);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, OutputOriginalData ? data: null, deserializeResult.Data, deserializeResult.Error);
}
else
{
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, OutputOriginalData ? desResult.OriginalData : null, desResult.Data, desResult.Error);
}
}
else
{
// Http status code indicates error
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
log.Write(LogLevel.Debug, $"[{request.RequestId}] Error received: {data}");
responseStream.Close();
response.Close();
var parseResult = ValidateJson(data);
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : parseResult.Error!;
if(error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(statusCode, headers, default, error);
}
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString();
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
return new WebCallResult<T>(null, null, default, new WebError(exceptionInfo));
}
catch (OperationCanceledException canceledException)
{
if (canceledException.CancellationToken == cancellationToken)
{
// Cancellation token cancelled by caller
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request cancel requested");
return new WebCallResult<T>(null, null, default, new CancellationRequestedError());
}
else
{
// Request timed out
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out");
return new WebCallResult<T>(null, null, default, new WebError($"[{request.RequestId}] Request timed out"));
}
}
}
/// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null
/// </summary>
/// <param name="data">Received data</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
{
return Task.FromResult<ServerError?>(null);
}
/// <summary>
/// Creates a request object
/// </summary>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed</param>
/// <param name="arraySerialization">How array parameters should be serialized</param>
/// <param name="requestId">Unique id of a request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
protected virtual IRequest ConstructRequest(
Uri uri,
HttpMethod method,
Dictionary<string, object>? parameters,
bool signed,
HttpMethodParameterPosition parameterPosition,
ArrayParametersSerialization arraySerialization,
int requestId,
Dictionary<string, string>? additionalHeaders)
{
if (parameters == null)
parameters = new Dictionary<string, object>();
var uriString = uri.ToString();
if (authProvider != null)
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, parameterPosition, arraySerialization);
if (parameterPosition == HttpMethodParameterPosition.InUri && parameters?.Any() == true)
uriString += "?" + parameters.CreateParamString(true, arraySerialization);
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
var request = RequestFactory.Create(method, uriString, requestId);
request.Accept = Constants.JsonContentHeader;
var headers = new Dictionary<string, string>();
if (authProvider != null)
headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, parameterPosition, arraySerialization);
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
if (additionalHeaders != null)
{
foreach (var header in additionalHeaders)
request.AddHeader(header.Key, header.Value);
}
if(StandardRequestHeaders != null)
{
foreach (var header in StandardRequestHeaders)
// Only add it if it isn't overwritten
if(additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
if (parameters?.Any() == true)
WriteParamBody(request, parameters, contentType);
else
request.SetContent(requestBodyEmptyContent, contentType);
}
return request;
}
/// <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, Dictionary<string, object> parameters, string contentType)
{
if (requestBodyFormat == RequestBodyFormat.Json)
{
// Write the parameters as json in the body
var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value));
request.SetContent(stringData, contentType);
}
else if (requestBodyFormat == RequestBodyFormat.FormData)
{
// Write the parameters as form data in the body
var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters.OrderBy(p => p.Key))
{
if (kvp.Value.GetType().IsArray)
{
var array = (Array)kvp.Value;
foreach (var value in array)
formData.Add(kvp.Key, value.ToString());
}
else
formData.Add(kvp.Key, kvp.Value.ToString());
}
var stringData = formData.ToString();
request.SetContent(stringData, contentType);
}
}
/// <summary>
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
/// </summary>
/// <param name="error">The string the request returned</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(JToken error)
{
return new ServerError(error.ToString());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,12 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public T Data { get; set; }
internal DataEvent(T data, DateTime timestamp)
/// <summary>
/// Ctor
/// </summary>
/// <param name="data"></param>
/// <param name="timestamp"></param>
public DataEvent(T data, DateTime timestamp)
{
Data = data;
Timestamp = timestamp;

View File

@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Sockets
public DateTime ReceivedTimestamp { get; set; }
/// <summary>
///
/// ctor
/// </summary>
/// <param name="connection"></param>
/// <param name="jsonData"></param>

View File

@ -0,0 +1,47 @@
using CryptoExchange.Net.Objects;
using Newtonsoft.Json.Linq;
using System;
using System.Threading;
namespace CryptoExchange.Net.Sockets
{
internal class PendingRequest
{
public Func<JToken, bool> Handler { get; }
public JToken? Result { get; private set; }
public bool Completed { get; private set; }
public AsyncResetEvent Event { get; }
public TimeSpan Timeout { get; }
private CancellationTokenSource cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
{
Handler = handler;
Event = new AsyncResetEvent(false, false);
Timeout = timeout;
cts = new CancellationTokenSource(timeout);
cts.Token.Register(Fail, false);
}
public bool CheckData(JToken data)
{
if (Handler(data))
{
Result = data;
Completed = true;
Event.Set();
return true;
}
return false;
}
public void Fail()
{
Completed = true;
Event.Set();
}
}
}

View File

@ -3,18 +3,18 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Objects;
using System.Net.WebSockets;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Socket connecting
/// A single socket connection to the server
/// </summary>
public class SocketConnection
{
@ -22,26 +22,27 @@ namespace CryptoExchange.Net.Sockets
/// Connection lost event
/// </summary>
public event Action? ConnectionLost;
/// <summary>
/// Connection closed and no reconnect is happening
/// </summary>
public event Action? ConnectionClosed;
/// <summary>
/// Connecting restored event
/// </summary>
public event Action<TimeSpan>? ConnectionRestored;
/// <summary>
/// The connection is paused event
/// </summary>
public event Action? ActivityPaused;
/// <summary>
/// The connection is unpaused event
/// </summary>
public event Action? ActivityUnpaused;
/// <summary>
/// Connecting closed event
/// </summary>
public event Action? Closed;
/// <summary>
/// Unhandled message event
/// </summary>
@ -57,35 +58,57 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// If connection is authenticated
/// Get a copy of the current subscriptions
/// </summary>
public bool Authenticated { get; set; }
public SocketSubscription[] Subscriptions
{
get
{
lock (subscriptionLock)
return subscriptions.Where(h => h.UserSubscription).ToArray();
}
}
/// <summary>
/// If the connection has been authenticated
/// </summary>
public bool Authenticated { get; internal set; }
/// <summary>
/// If connection is made
/// </summary>
public bool Connected { get; private set; }
public bool Connected => _socket.IsOpen;
/// <summary>
/// The underlying socket
/// The unique ID of the socket
/// </summary>
public IWebsocket Socket { get; set; }
public int SocketId => _socket.Id;
/// <summary>
/// If the socket should be reconnected upon closing
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
/// </summary>
public bool ShouldReconnect { get; set; }
public double IncomingKbps => _socket.IncomingKbps;
/// <summary>
/// Current reconnect try
/// The connection uri
/// </summary>
public int ReconnectTry { get; set; }
public Uri ConnectionUri => _socket.Uri;
/// <summary>
/// Current resubscribe try
/// The API client the connection is for
/// </summary>
public int ResubscribeTry { get; set; }
public SocketApiClient ApiClient { get; set; }
/// <summary>
/// Time of disconnecting
/// </summary>
public DateTime? DisconnectTime { get; set; }
/// <summary>
/// Tag for identificaion
/// </summary>
public string Tag { get; set; }
/// <summary>
/// If activity is paused
/// </summary>
@ -97,52 +120,172 @@ namespace CryptoExchange.Net.Sockets
if (pausedActivity != value)
{
pausedActivity = value;
log.Write(LogLevel.Debug, $"Socket {Socket.Id} Paused activity: " + value);
if(pausedActivity) ActivityPaused?.Invoke();
else ActivityUnpaused?.Invoke();
log.Write(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value);
if(pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke());
else _ = Task.Run(() => ActivityUnpaused?.Invoke());
}
}
}
/// <summary>
/// Status of the socket connection
/// </summary>
public SocketStatus Status
{
get => _status;
private set
{
if (_status == value)
return;
var oldStatus = _status;
_status = value;
log.Write(LogLevel.Debug, $"Socket {SocketId} status changed from {oldStatus} to {_status}");
}
}
private bool pausedActivity;
private readonly List<SocketSubscription> subscriptions;
private readonly object subscriptionLock = new object();
private readonly object subscriptionLock = new();
private bool lostTriggered;
private readonly Log log;
private readonly SocketClient socketClient;
private readonly BaseSocketClient socketClient;
private readonly List<PendingRequest> pendingRequests;
private SocketStatus _status;
/// <summary>
/// The underlying websocket
/// </summary>
private readonly IWebsocket _socket;
/// <summary>
/// New socket connection
/// </summary>
/// <param name="client">The socket client</param>
/// <param name="apiClient">The api client</param>
/// <param name="socket">The socket</param>
public SocketConnection(SocketClient client, IWebsocket socket)
/// <param name="tag"></param>
public SocketConnection(BaseSocketClient client, SocketApiClient apiClient, IWebsocket socket, string tag)
{
log = client.log;
socketClient = client;
ApiClient = apiClient;
Tag = tag;
pendingRequests = new List<PendingRequest>();
subscriptions = new List<SocketSubscription>();
Socket = socket;
Socket.Timeout = client.SocketNoDataTimeout;
Socket.OnMessage += ProcessMessage;
Socket.OnClose += SocketOnClose;
Socket.OnOpen += SocketOnOpen;
_socket = socket;
_socket.OnMessage += HandleMessage;
_socket.OnOpen += HandleOpen;
_socket.OnClose += HandleClose;
_socket.OnReconnecting += HandleReconnecting;
_socket.OnReconnected += HandleReconnected;
_socket.OnError += HandleError;
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
}
/// <summary>
/// Handler for a socket opening
/// </summary>
protected virtual void HandleOpen()
{
Status = SocketStatus.Connected;
PausedActivity = false;
}
/// <summary>
/// Handler for a socket closing without reconnect
/// </summary>
protected virtual void HandleClose()
{
Status = SocketStatus.Closed;
Authenticated = false;
lock(subscriptionLock)
{
foreach (var sub in subscriptions)
sub.Confirmed = false;
}
Task.Run(() => ConnectionClosed?.Invoke());
}
/// <summary>
/// Handler for a socket losing conenction and starting reconnect
/// </summary>
protected virtual void HandleReconnecting()
{
Status = SocketStatus.Reconnecting;
DisconnectTime = DateTime.UtcNow;
Authenticated = false;
lock (subscriptionLock)
{
foreach (var sub in subscriptions)
sub.Confirmed = false;
}
_ = Task.Run(() => ConnectionLost?.Invoke());
}
/// <summary>
/// Get the url to connect to when reconnecting
/// </summary>
/// <returns></returns>
protected virtual async Task<Uri?> GetReconnectionUrlAsync()
{
return await socketClient.GetReconnectUriAsync(ApiClient, this).ConfigureAwait(false);
}
/// <summary>
/// Handler for a socket which has reconnected
/// </summary>
protected virtual async void HandleReconnected()
{
Status = SocketStatus.Resubscribing;
lock (pendingRequests)
{
foreach (var pendingRequest in pendingRequests.ToList())
{
pendingRequest.Fail();
pendingRequests.Remove(pendingRequest);
}
}
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful)
await _socket.ReconnectAsync().ConfigureAwait(false);
else
{
Status = SocketStatus.Connected;
_ = Task.Run(() =>
{
ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value);
DisconnectTime = null;
});
}
}
/// <summary>
/// Handler for an error on a websocket
/// </summary>
/// <param name="e">The exception</param>
protected virtual void HandleError(Exception e)
{
if (e is WebSocketException wse)
log.Write(LogLevel.Warning, $"Socket {SocketId} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
else
log.Write(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
}
/// <summary>
/// Process a message received by the socket
/// </summary>
/// <param name="data"></param>
private void ProcessMessage(string data)
/// <param name="data">The received data</param>
protected virtual void HandleMessage(string data)
{
var timestamp = DateTime.UtcNow;
log.Write(LogLevel.Trace, $"Socket {Socket.Id} received data: " + data);
log.Write(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
if (string.IsNullOrEmpty(data)) return;
var tokenData = data.ToJToken(log);
@ -155,15 +298,13 @@ namespace CryptoExchange.Net.Sockets
}
var handledResponse = false;
PendingRequest[] requests;
lock(pendingRequests)
requests = pendingRequests.ToArray();
// Remove any timed out requests
foreach (var request in requests.Where(r => r.Completed))
PendingRequest[] requests;
lock (pendingRequests)
{
lock (pendingRequests)
pendingRequests.Remove(request);
pendingRequests.RemoveAll(r => r.Completed);
requests = pendingRequests.ToArray();
}
// Check if this message is an answer on any pending requests
@ -171,7 +312,7 @@ namespace CryptoExchange.Net.Sockets
{
if (pendingRequest.CheckData(tokenData))
{
lock (pendingRequests)
lock (pendingRequests)
pendingRequests.Remove(pendingRequest);
if (!socketClient.ContinueOnQueryResponse)
@ -183,42 +324,175 @@ namespace CryptoExchange.Net.Sockets
}
// Message was not a request response, check data handlers
var messageEvent = new MessageEvent(this, tokenData, socketClient.OutputOriginalData ? data: null, timestamp);
if (!HandleData(messageEvent) && !handledResponse)
var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data : null, timestamp);
var (handled, userProcessTime, subscription) = HandleData(messageEvent);
if (!handled && !handledResponse)
{
if (!socketClient.UnhandledMessageExpected)
log.Write(LogLevel.Warning, $"Socket {Socket.Id} Message not handled: " + tokenData);
log.Write(LogLevel.Warning, $"Socket {SocketId} Message not handled: " + tokenData);
UnhandledMessage?.Invoke(tokenData);
}
var total = DateTime.UtcNow - timestamp;
if (userProcessTime.TotalMilliseconds > 500)
log.Write(LogLevel.Debug, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processing slow ({(int)total.TotalMilliseconds}ms, {(int)userProcessTime.TotalMilliseconds}ms user code), consider offloading data handling to another thread. " +
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
log.Write(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)");
}
/// <summary>
/// Connect the websocket
/// </summary>
/// <returns></returns>
public async Task<bool> ConnectAsync() => await _socket.ConnectAsync().ConfigureAwait(false);
/// <summary>
/// Retrieve the underlying socket
/// </summary>
/// <returns></returns>
public IWebsocket GetSocket() => _socket;
/// <summary>
/// Trigger a reconnect of the socket connection
/// </summary>
/// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
/// <summary>
/// Close the connection
/// </summary>
/// <returns></returns>
public async Task CloseAsync()
{
if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
return;
if (socketClient.socketConnections.ContainsKey(SocketId))
socketClient.socketConnections.TryRemove(SocketId, out _);
lock (subscriptionLock)
{
foreach (var subscription in subscriptions)
{
if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose();
}
}
await _socket.CloseAsync().ConfigureAwait(false);
_socket.Dispose();
}
/// <summary>
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
/// </summary>
/// <param name="subscription">Subscription to close</param>
/// <returns></returns>
public async Task CloseAsync(SocketSubscription subscription)
{
lock (subscriptionLock)
{
if (!subscriptions.Contains(subscription))
return;
subscription.Closed = true;
}
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
return;
log.Write(LogLevel.Debug, $"Socket {SocketId} closing subscription {subscription.Id}");
if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose();
if (subscription.Confirmed && _socket.IsOpen)
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
bool shouldCloseConnection;
lock (subscriptionLock)
{
if (Status == SocketStatus.Closing)
{
log.Write(LogLevel.Debug, $"Socket {SocketId} already closing");
return;
}
shouldCloseConnection = subscriptions.All(r => !r.UserSubscription || r.Closed);
if (shouldCloseConnection)
Status = SocketStatus.Closing;
}
if (shouldCloseConnection)
{
log.Write(LogLevel.Debug, $"Socket {SocketId} closing as there are no more subscriptions");
await CloseAsync().ConfigureAwait(false);
}
lock (subscriptionLock)
subscriptions.Remove(subscription);
}
/// <summary>
/// Dispose the connection
/// </summary>
public void Dispose()
{
Status = SocketStatus.Disposed;
_socket.Dispose();
}
/// <summary>
/// Add a subscription to this connection
/// </summary>
/// <param name="subscription"></param>
public bool AddSubscription(SocketSubscription subscription)
{
lock (subscriptionLock)
{
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
return false;
subscriptions.Add(subscription);
if(subscription.UserSubscription)
log.Write(LogLevel.Debug, $"Socket {SocketId} adding new subscription with id {subscription.Id}, total subscriptions on connection: {subscriptions.Count(s => s.UserSubscription)}");
return true;
}
}
/// <summary>
/// Add subscription to this connection
/// </summary>
/// <param name="subscription"></param>
public void AddSubscription(SocketSubscription subscription)
{
lock(subscriptionLock)
subscriptions.Add(subscription);
}
/// <summary>
/// Get a subscription on this connection
/// Get a subscription on this connection by id
/// </summary>
/// <param name="id"></param>
public SocketSubscription GetSubscription(int id)
public SocketSubscription? GetSubscription(int id)
{
lock (subscriptionLock)
return subscriptions.SingleOrDefault(s => s.Id == id);
}
private bool HandleData(MessageEvent messageEvent)
/// <summary>
/// Get a subscription on this connection by its subscribe request
/// </summary>
/// <param name="predicate">Filter for a request</param>
/// <returns></returns>
public SocketSubscription? GetSubscriptionByRequest(Func<object?, bool> predicate)
{
lock(subscriptionLock)
return subscriptions.SingleOrDefault(s => predicate(s.Request));
}
/// <summary>
/// Process data
/// </summary>
/// <param name="messageEvent"></param>
/// <returns>True if the data was successfully handled</returns>
private (bool, TimeSpan, SocketSubscription?) HandleData(MessageEvent messageEvent)
{
SocketSubscription? currentSubscription = null;
try
{
var handled = false;
var sw = Stopwatch.StartNew();
TimeSpan userCodeDuration = TimeSpan.Zero;
// Loop the subscriptions to check if any of them signal us that the message is for them
List<SocketSubscription> subscriptionsCopy;
@ -230,36 +504,36 @@ namespace CryptoExchange.Net.Sockets
currentSubscription = subscription;
if (subscription.Request == null)
{
if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Identifier!))
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!))
{
handled = true;
var userSw = Stopwatch.StartNew();
subscription.MessageHandler(messageEvent);
userSw.Stop();
userCodeDuration = userSw.Elapsed;
}
}
else
{
if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Request))
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Request))
{
handled = true;
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData);
var userSw = Stopwatch.StartNew();
subscription.MessageHandler(messageEvent);
userSw.Stop();
userCodeDuration = userSw.Elapsed;
}
}
}
sw.Stop();
if (sw.ElapsedMilliseconds > 500)
log.Write(LogLevel.Warning, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " +
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
else
log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms");
return handled;
return (handled, userCodeDuration, currentSubscription);
}
catch (Exception ex)
{
log.Write(LogLevel.Error, $"Socket {Socket.Id} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
log.Write(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
currentSubscription?.InvokeExceptionHandler(ex);
return false;
return (false, TimeSpan.Zero, null);
}
}
@ -269,7 +543,7 @@ namespace CryptoExchange.Net.Sockets
/// <typeparam name="T">The data type expected in response</typeparam>
/// <param name="obj">The object to send</param>
/// <param name="timeout">The timeout for response</param>
/// <param name="handler">The response handler</param>
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
/// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
{
@ -278,7 +552,10 @@ namespace CryptoExchange.Net.Sockets
{
pendingRequests.Add(pending);
}
Send(obj);
var sendOk = Send(obj);
if(!sendOk)
pending.Fail();
return pending.Event.WaitAsync(timeout);
}
@ -288,214 +565,99 @@ namespace CryptoExchange.Net.Sockets
/// <typeparam name="T">The type of the object to send</typeparam>
/// <param name="obj">The object to send</param>
/// <param name="nullValueHandling">How null values should be serialized</param>
public virtual void Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
public virtual bool Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
{
if(obj is string str)
Send(str);
return Send(str);
else
Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
}
/// <summary>
/// Send string data over the websocket connection
/// </summary>
/// <param name="data">The data to send</param>
public virtual void Send(string data)
public virtual bool Send(string data)
{
log.Write(LogLevel.Debug, $"Socket {Socket.Id} sending data: {data}");
Socket.Send(data);
}
/// <summary>
/// Handler for a socket opening
/// </summary>
protected virtual void SocketOnOpen()
{
ReconnectTry = 0;
PausedActivity = false;
Connected = true;
}
/// <summary>
/// Handler for a socket closing. Reconnects the socket if needed, or removes it from the active socket list if not
/// </summary>
protected virtual void SocketOnClose()
{
lock (pendingRequests)
log.Write(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
try
{
foreach(var pendingRequest in pendingRequests.ToList())
{
pendingRequest.Fail();
pendingRequests.Remove(pendingRequest);
}
_socket.Send(data);
return true;
}
if (socketClient.AutoReconnect && ShouldReconnect)
catch(Exception)
{
if (Socket.Reconnecting)
return; // Already reconnecting
Socket.Reconnecting = true;
DisconnectTime = DateTime.UtcNow;
log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}");
if (!lostTriggered)
{
lostTriggered = true;
ConnectionLost?.Invoke();
}
Task.Run(async () =>
{
while (ShouldReconnect)
{
// Wait a bit before attempting reconnect
await Task.Delay(socketClient.ReconnectInterval).ConfigureAwait(false);
if (!ShouldReconnect)
{
// Should reconnect changed to false while waiting to reconnect
Socket.Reconnecting = false;
return;
}
Socket.Reset();
if (!await Socket.ConnectAsync().ConfigureAwait(false))
{
ReconnectTry++;
ResubscribeTry = 0;
if (socketClient.MaxReconnectTries != null
&& ReconnectTry >= socketClient.MaxReconnectTries)
{
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect after {ReconnectTry} tries, closing");
ShouldReconnect = false;
if (socketClient.sockets.ContainsKey(Socket.Id))
socketClient.sockets.TryRemove(Socket.Id, out _);
Closed?.Invoke();
_ = Task.Run(() => ConnectionClosed?.Invoke());
break;
}
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect{(socketClient.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.MaxReconnectTries}": "")}");
continue;
}
// Successfully reconnected
var time = DisconnectTime;
DisconnectTime = null;
log.Write(LogLevel.Information, $"Socket {Socket.Id} reconnected after {DateTime.UtcNow - time}");
var reconnectResult = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectResult)
{
ResubscribeTry++;
if (socketClient.MaxResubscribeTries != null &&
ResubscribeTry >= socketClient.MaxResubscribeTries)
{
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to resubscribe after {ResubscribeTry} tries, closing");
ShouldReconnect = false;
if (socketClient.sockets.ContainsKey(Socket.Id))
socketClient.sockets.TryRemove(Socket.Id, out _);
Closed?.Invoke();
_ = Task.Run(() => ConnectionClosed?.Invoke());
}
else
log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket{(socketClient.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.MaxResubscribeTries}" : "")}. Disconnecting and reconnecting.");
if (Socket.IsOpen)
await Socket.CloseAsync().ConfigureAwait(false);
else
DisconnectTime = DateTime.UtcNow;
}
else
{
log.Write(LogLevel.Debug, $"Socket {Socket.Id} data connection restored.");
ResubscribeTry = 0;
if (lostTriggered)
{
lostTriggered = false;
InvokeConnectionRestored(time);
}
break;
}
}
Socket.Reconnecting = false;
});
}
else
{
if (!socketClient.AutoReconnect && ShouldReconnect)
_ = Task.Run(() => ConnectionClosed?.Invoke());
// No reconnecting needed
log.Write(LogLevel.Information, $"Socket {Socket.Id} closed");
if (socketClient.sockets.ContainsKey(Socket.Id))
socketClient.sockets.TryRemove(Socket.Id, out _);
Closed?.Invoke();
return false;
}
}
private async void InvokeConnectionRestored(DateTime? disconnectTime)
private async Task<CallResult<bool>> ProcessReconnectAsync()
{
await Task.Run(() => ConnectionRestored?.Invoke(disconnectTime.HasValue ? DateTime.UtcNow - disconnectTime.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false);
}
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
private async Task<bool> ProcessReconnectAsync()
{
if (Authenticated)
bool anySubscriptions = false;
lock (subscriptionLock)
anySubscriptions = subscriptions.Any(s => s.UserSubscription);
if (!anySubscriptions)
{
if (!Socket.IsOpen)
return false;
// No need to resubscribe anything
log.Write(LogLevel.Debug, $"Socket {SocketId} Nothing to resubscribe, closing connection");
_ = _socket.CloseAsync();
return new CallResult<bool>(true);
}
if (subscriptions.Any(s => s.Authenticated))
{
// If we reconnected a authenticated connection we need to re-authenticate
var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
if (!authResult)
{
log.Write(LogLevel.Information, $"Socket {Socket.Id} authentication failed on reconnected socket. Disconnecting and reconnecting.");
return false;
log.Write(LogLevel.Warning, $"Socket {SocketId} authentication failed on reconnected socket. Disconnecting and reconnecting.");
return authResult;
}
log.Write(LogLevel.Debug, $"Socket {Socket.Id} authentication succeeded on reconnected socket.");
Authenticated = true;
log.Write(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
}
// Get a list of all subscriptions on the socket
List<SocketSubscription> subscriptionList;
List<SocketSubscription> subscriptionList = new List<SocketSubscription>();
lock (subscriptionLock)
subscriptionList = subscriptions.Where(h => h.Request != null).ToList();
{
foreach (var subscription in subscriptions)
{
if (subscription.Request != null)
subscriptionList.Add(subscription);
else
subscription.Confirmed = true;
}
}
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
for (var i = 0; i < subscriptionList.Count; i += socketClient.MaxConcurrentResubscriptionsPerSocket)
for (var i = 0; i < subscriptionList.Count; i += socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
{
var success = true;
var taskList = new List<Task>();
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.MaxConcurrentResubscriptionsPerSocket))
{
if (!Socket.IsOpen)
continue;
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
var task = socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription).ContinueWith(t =>
{
if (!t.Result)
success = false;
});
taskList.Add(task);
}
var taskList = new List<Task<CallResult<bool>>>();
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
taskList.Add(socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription));
await Task.WhenAll(taskList).ConfigureAwait(false);
if (!success || !Socket.IsOpen)
return false;
}
if (taskList.Any(t => !t.Result.Success))
return taskList.First(t => !t.Result.Success).Result;
}
log.Write(LogLevel.Debug, $"Socket {Socket.Id} all subscription successfully resubscribed on reconnected socket.");
return true;
foreach (var subscription in subscriptionList)
subscription.Confirmed = true;
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
log.Write(LogLevel.Debug, $"Socket {SocketId} all subscription successfully resubscribed on reconnected socket.");
return new CallResult<bool>(true);
}
internal async Task UnsubscribeAsync(SocketSubscription socketSubscription)
@ -505,89 +667,45 @@ namespace CryptoExchange.Net.Sockets
internal async Task<CallResult<bool>> ResubscribeAsync(SocketSubscription socketSubscription)
{
if (!Socket.IsOpen)
return new CallResult<bool>(false, new UnknownError("Socket is not connected"));
if (!_socket.IsOpen)
return new CallResult<bool>(new UnknownError("Socket is not connected"));
return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
}
/// <summary>
/// Close the connection
/// </summary>
/// <returns></returns>
public async Task CloseAsync()
{
Connected = false;
ShouldReconnect = false;
if (socketClient.sockets.ContainsKey(Socket.Id))
socketClient.sockets.TryRemove(Socket.Id, out _);
await Socket.CloseAsync().ConfigureAwait(false);
Socket.Dispose();
}
/// <summary>
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
/// Status of the socket connection
/// </summary>
/// <param name="subscription">Subscription to close</param>
/// <returns></returns>
public async Task CloseAsync(SocketSubscription subscription)
public enum SocketStatus
{
if (!Socket.IsOpen)
return;
if (subscription.Confirmed)
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
var shouldCloseConnection = false;
lock (subscriptionLock)
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
if (shouldCloseConnection)
await CloseAsync().ConfigureAwait(false);
lock (subscriptionLock)
subscriptions.Remove(subscription);
}
}
internal class PendingRequest
{
public Func<JToken, bool> Handler { get; }
public JToken? Result { get; private set; }
public bool Completed { get; private set; }
public AsyncResetEvent Event { get; }
public TimeSpan Timeout { get; }
private CancellationTokenSource cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
{
Handler = handler;
Event = new AsyncResetEvent(false, false);
Timeout = timeout;
cts = new CancellationTokenSource(timeout);
cts.Token.Register(Fail, false);
}
public bool CheckData(JToken data)
{
if (Handler(data))
{
Result = data;
Completed = true;
Event.Set();
return true;
}
return false;
}
public void Fail()
{
Completed = true;
Event.Set();
/// <summary>
/// None/Initial
/// </summary>
None,
/// <summary>
/// Connected
/// </summary>
Connected,
/// <summary>
/// Reconnecting
/// </summary>
Reconnecting,
/// <summary>
/// Resubscribing on reconnected socket
/// </summary>
Resubscribing,
/// <summary>
/// Closing
/// </summary>
Closing,
/// <summary>
/// Closed
/// </summary>
Closed,
/// <summary>
/// Disposed
/// </summary>
Disposed
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Threading;
namespace CryptoExchange.Net.Sockets
{
@ -8,9 +9,10 @@ namespace CryptoExchange.Net.Sockets
public class SocketSubscription
{
/// <summary>
/// Subscription id
/// Unique subscription id
/// </summary>
public int Id { get; }
/// <summary>
/// Exception event
/// </summary>
@ -22,44 +24,64 @@ namespace CryptoExchange.Net.Sockets
public Action<MessageEvent> MessageHandler { get; set; }
/// <summary>
/// Request object
/// The request object send when subscribing on the server. Either this or the `Identifier` property should be set
/// </summary>
public object? Request { get; set; }
/// <summary>
/// Subscription identifier
/// The subscription identifier, used instead of a `Request` object to identify the subscription
/// </summary>
public string? Identifier { get; set; }
/// <summary>
/// Is user subscription or generic
/// Whether this is a user subscription or an internal listener
/// </summary>
public bool UserSubscription { get; set; }
/// <summary>
/// If the subscription has been confirmed
/// If the subscription has been confirmed to be subscribed by the server
/// </summary>
public bool Confirmed { get; set; }
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, Action<MessageEvent> dataHandler)
/// <summary>
/// Whether authentication is needed for this subscription
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Whether we're closing this subscription and a socket connection shouldn't be kept open for it
/// </summary>
public bool Closed { get; set; }
/// <summary>
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
/// a provided cancelation token
/// </summary>
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, bool authenticated, Action<MessageEvent> dataHandler)
{
Id = id;
UserSubscription = userSubscription;
MessageHandler = dataHandler;
Request = request;
Identifier = identifier;
Authenticated = authenticated;
}
/// <summary>
/// Create SocketSubscription for a request
/// Create SocketSubscription for a subscribe request
/// </summary>
/// <param name="id"></param>
/// <param name="request"></param>
/// <param name="userSubscription"></param>
/// <param name="authenticated"></param>
/// <param name="dataHandler"></param>
/// <returns></returns>
public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription,
Action<MessageEvent> dataHandler)
bool authenticated, Action<MessageEvent> dataHandler)
{
return new SocketSubscription(id, request, null, userSubscription, dataHandler);
return new SocketSubscription(id, request, null, userSubscription, authenticated, dataHandler);
}
/// <summary>
@ -68,12 +90,13 @@ namespace CryptoExchange.Net.Sockets
/// <param name="id"></param>
/// <param name="identifier"></param>
/// <param name="userSubscription"></param>
/// <param name="authenticated"></param>
/// <param name="dataHandler"></param>
/// <returns></returns>
public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription,
Action<MessageEvent> dataHandler)
bool authenticated, Action<MessageEvent> dataHandler)
{
return new SocketSubscription(id, null, identifier, userSubscription, dataHandler);
return new SocketSubscription(id, null, identifier, userSubscription, authenticated, dataHandler);
}
/// <summary>

View File

@ -22,8 +22,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the <see cref="SocketClientOptions.MaxReconnectTries"/> and <see cref="SocketClientOptions.MaxResubscribeTries"/> options,
/// or <see cref="SocketClientOptions.AutoReconnect"/> is false
/// Event when the connection is closed and will not be reconnected
/// </summary>
public event Action ConnectionClosed
{
@ -33,8 +32,8 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting.
/// Note that when the executing code is suspended and resumed at a later period (for example laptop going to sleep) the disconnect time will be incorrect as the diconnect
/// will only be detected after resuming. This will lead to an incorrect disconnected timespan.
/// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the diconnect
/// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes.
/// </summary>
public event Action<TimeSpan> ConnectionRestored
{
@ -72,7 +71,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// The id of the socket
/// </summary>
public int SocketId => connection.Socket.Id;
public int SocketId => connection.SocketId;
/// <summary>
/// The id of the subscription
@ -103,9 +102,9 @@ namespace CryptoExchange.Net.Sockets
/// Close the socket to cause a reconnect
/// </summary>
/// <returns></returns>
internal Task ReconnectAsync()
public Task ReconnectAsync()
{
return connection.Socket.CloseAsync();
return connection.TriggerReconnectAsync();
}
/// <summary>

View File

@ -0,0 +1,80 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Parameters for a websocket
/// </summary>
public class WebSocketParameters
{
/// <summary>
/// The uri to connect to
/// </summary>
public Uri Uri { get; set; }
/// <summary>
/// Headers to send in the connection handshake
/// </summary>
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Cookies to send in the connection handshake
/// </summary>
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
/// <summary>
/// The time to wait between reconnect attempts
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Proxy for the connection
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Whether the socket should automatically reconnect when connection is lost
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
/// </summary>
public TimeSpan? Timeout { get; set; }
/// <summary>
/// Interval at which to send ping frames
/// </summary>
public TimeSpan? KeepAliveInterval { get; set; }
/// <summary>
/// The max amount of messages to send per second
/// </summary>
public int? RatelimitPerSecond { get; set; }
/// <summary>
/// Origin header value to send in the connection handshake
/// </summary>
public string? Origin { get; set; }
/// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
public Func<byte[], string>? DataInterpreterBytes { get; set; }
/// <summary>
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// </summary>
public Func<string, string>? DataInterpreterString { get; set; }
/// <summary>
/// Encoding for sending/receiving data
/// </summary>
public Encoding Encoding { get; set; } = Encoding.UTF8;
/// <summary>
/// ctor
/// </summary>
/// <param name="uri">Uri</param>
/// <param name="autoReconnect">Auto reconnect</param>
public WebSocketParameters(Uri uri, bool autoReconnect)
{
Uri = uri;
AutoReconnect = autoReconnect;
}
}
}

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