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

Squashed commit of the following:

commit 1ca93ce870468c7effcc69a05b1675f855598f3e
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Aug 12 13:44:03 2021 +0200

    Version 4.0.0 release

commit f8cefe72cae8b845aa995d16415f160228f5dfc4
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Aug 9 11:46:50 2021 +0200

    Fix for processing a duplicate message in symbol order book

commit f3573e414f80ea622caadb38f9e020f2eab7c834
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Aug 9 11:36:50 2021 +0200

    Updated version

commit 170a3b5c83cd500f0adb5cf74eaaa9c1a8674a10
Merge: 73a0e2c 0d43f6e
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Aug 9 11:33:59 2021 +0200

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

commit 73a0e2cb62f8d51b4b8fe1b29fbf16f07ebf13e0
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Aug 9 11:33:45 2021 +0200

    Fixed processing issue in SymbolOrderBook

commit 0d43f6e14e9b61af9bb3250fd72559a5fbf5300a
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jul 31 14:48:23 2021 +0200

    Updated version

commit 037d765fc73740d2aa2159bce6d57f01d989bdc2
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jul 31 11:14:42 2021 +0200

    Socket fixes

commit 13cf3e27b9e91b252368a5dc799363111eed9148
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jul 26 09:55:51 2021 +0200

    Updated version

commit 333c6c4207cafbfd09b37743fb36008f87939666
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jul 26 09:50:35 2021 +0200

    Update CryptoExchangeWebSocketClient.cs

commit ad2b6284b042cfa846e55f8b5f53b2397039dff7
Merge: 64bdcdf 2591cc6
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sun Jul 25 22:44:49 2021 +0200

    Merge branch 'feature/replace-websocket4net' of https://github.com/jkorf/CryptoExchange.Net into feature/replace-websocket4net

commit 64bdcdf087d56b8470baedac1ac86d263ad72490
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sun Jul 25 22:44:03 2021 +0200

    Fix for deadlock

commit 2591cc6782e281595c436ccb4b74a9c7c7f5dc57
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jul 15 15:10:08 2021 +0200

    Cleanup

commit b3b6235b6edf0ea1a6437239919666d08c274129
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jul 9 16:14:33 2021 +0200

    Updated version

commit 12975fd13f7884382fea608d774b1e0ac384cb70
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jul 9 16:12:04 2021 +0200

    Async postfix for order book methods

commit d49d59094e18c83de994a1b591f906f7efe916c5
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jul 9 15:47:38 2021 +0200

    Renamed async methods with Async post fixes

commit cbe103930ab90378507780cb150596abcb15eb3f
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jul 8 09:29:34 2021 +0200

    Added Book property on SymbolOrderBook

commit ecc101aed11bab73c49fadc93d3ca4607094d92a
Merge: 181f942 a172461
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Jul 7 23:38:58 2021 +0200

    Merge branch 'feature/replace-websocket4net' of https://github.com/jkorf/CryptoExchange.Net into feature/replace-websocket4net

commit 181f942e2137ff5dfede5365012e24dfb2f07cf9
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Jul 7 23:38:49 2021 +0200

    Added CalculateAverageFillPrice to order book

commit a17246153df463aa881e9808ffb87fa25f144d90
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Jul 7 13:11:39 2021 +0200

    Updated version

commit c60f138ae846a991662e7544d45845decd25cc10
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Jul 7 13:05:44 2021 +0200

    Fix not logging responses with LogLevel.Debug

commit 91f168fd25150b3048346bcc6acc549ee3685e50
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jul 5 10:39:48 2021 +0200

    Added ExchangeHelpers to ReadMe

commit ff6c02e574d3f8a270e2960fdda0222c53457868
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jul 5 10:34:25 2021 +0200

    Added some helper methods, some code comments

commit 55900a3db938f032436303c2399f41b7a3a5fef1
Merge: 9067a8a 7ab6bb0
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jul 5 09:24:43 2021 +0200

    Merge branch 'master' into feature/replace-websocket4net

commit 9067a8a82c5d8c1e1f6ba0f2600539efd7f1a122
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jul 2 16:03:38 2021 +0200

    Update README.md

commit 0f202d69c7e3db71f66dcfef5673b97f56451a40
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jun 25 16:37:06 2021 +0200

    Code comments, added console logger

commit 52160c6dc1bbaca3cc498c49ec0dd71310d5102b
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jun 24 16:47:04 2021 +0200

    Added/cleaned up code comments

commit 95771c5c4ae3a74963fea5a01e05559596b1b39d
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jun 22 14:22:15 2021 +0200

    Additional info socket connection error

commit 6a90e2316d54d42c0f5b7772d60d4861ff340c5a
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jun 17 16:10:43 2021 +0200

    Updated version

commit b138c1050372b5d5fbdc3b2a33d3c09b677a669a
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jun 17 16:08:19 2021 +0200

    Fix not receiving OriginalData in output

commit 462f44cf45e1bc7ee333e37ad0a778185d84fa58
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Jun 16 22:35:03 2021 +0200

    Fix invalidoperation in order book

commit aa7414321b1297487a5133fa0a7e5ede059a6939
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sun Jun 13 20:56:01 2021 +0200

    logging no data task

commit 22aa5928f5cb11ac5877bc4c606d64550aad3f0c
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue Jun 8 19:13:18 2021 +0200

    Updated version

commit 1ceb22994a5cdf779de8890e39101956728ab80c
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue Jun 8 19:11:20 2021 +0200

    Fixed exception on .net framework when creating socket

commit 971cab739dd543de0122c9bc12819f57d4df15e3
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jun 7 14:41:27 2021 +0200

    Updated unit test packages

commit 216213d5ad0e14c6cd7b8e22131b8b07338bacfc
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jun 7 10:46:57 2021 +0200

    Updated version

commit ce2c7ddc182599c5ca9ef0eb64e14906c8b734e1
Merge: b219705 55aa77e
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jun 7 09:29:57 2021 +0200

    Merge pull request #96 from ridicoulous/master

    iexchange client improvements

commit b21970552602eb853a061034a5c223a381e7465b
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jun 1 14:55:02 2021 +0200

    Updated version

commit dd2cf84c163920e545ec09e1b0d29ab295f2fbe2
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jun 1 14:52:55 2021 +0200

    Added tests for LogLevel null

commit 975003284f53094750ec3d4a608d814a6d70f745
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jun 1 14:42:59 2021 +0200

    Made LogLevel nullable in options, moved log formatting to the ILogger implementation

commit 1c1de5651e31e9288b4fd125b0545efbfa9098f7
Merge: 34bf986 e7838ab
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon May 31 10:16:52 2021 +0200

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

commit 34bf9867dcd2c2b195079094560351d0db2dac0f
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon May 31 10:16:49 2021 +0200

    fixed some axync issues

commit e7838ab9e8310b4ae02c172f0a764aad8ca8fd4e
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed May 26 13:46:40 2021 +0200

    Added Discord link to ReadMe

commit 502939f57cf8deac1809c4bd2c63af4acc62bd43
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed May 26 11:12:59 2021 +0200

    Updated version

commit 92b745ca291259eb246572c8029709b87ddbf2d3
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue May 25 21:52:47 2021 +0200

    Refactored logging to use ILogger

commit b4904b5e4a239ffb29f00b13a314fea33d45d9cb
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue May 25 14:07:01 2021 +0200

    Added optional json output, added DataEvent for socket updates

commit 55aa77e92647ccdb00abdb35a7ac84f1317b9742
Author: Artem Kurianov <artemkuryanov@gmail.com>
Date:   Fri May 14 13:11:05 2021 +0000

    added order time

commit efe70445ed13228425278e6f57fb17d4156c12f4
Author: Artem Kurianov <artemkuryanov@gmail.com>
Date:   Fri May 14 13:08:58 2021 +0000

    iexchange client improvements

commit 8f0943f0f0469b42f40334257f0f3ca5e39e3bd9
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu May 6 10:24:33 2021 +0200

    Updated version

commit 17d1e5f71bd1986eea0e8273da070f3615d3e378
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu May 6 10:01:24 2021 +0200

    Added missing configureawaits

commit ed748aa474e0d52a78bc15b9b0aa0a9b4d8dd35e
Author: Jan Korf <jankorf91@gmail.com>
Date:   Fri Apr 30 23:00:07 2021 +0200

    Updated version

commit 1ae545563497889c75ed6183377e759da5beed61
Author: Jan Korf <jankorf91@gmail.com>
Date:   Fri Apr 30 21:00:40 2021 +0200

    Updated socket closing

commit 8243ad60dc64f96ed262ec54cbd6c55553c056be
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Apr 30 15:48:36 2021 +0200

    Updated version

commit 4299f238f3debc2b1b062eb7cde195a3d64dbdfc
Author: Jan Korf <jankorf91@gmail.com>
Date:   Fri Apr 30 15:44:32 2021 +0200

    Fix for closing socket without timeout task

commit 71f49cfe5142cb70fb651936e74b8eef5b7350d4
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Apr 30 14:59:10 2021 +0200

    Updated version

commit 5d669068ec0a7241daef383c6867c2e22ec21ed3
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Apr 30 14:56:15 2021 +0200

    Renaming, cleanup

    handler -> subscription
    socket (SocketConnection) -> socketConnection

commit 0fa13e286060cd3b96b808406434092c0e89e89d
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Apr 29 16:20:41 2021 +0200

    Initial work replacing Websocket4Net with ClientWebSocket
This commit is contained in:
Jkorf 2021-08-12 13:46:39 +02:00
parent 7c10c58779
commit dcb2fa35af
61 changed files with 2696 additions and 1655 deletions

1
.gitignore vendored
View File

@ -286,3 +286,4 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
CryptoExchange.Net/CryptoExchange.Net.xml

View File

@ -1,11 +1,10 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace CryptoExchange.Net.UnitTests
{
@ -30,54 +29,58 @@ namespace CryptoExchange.Net.UnitTests
public void SettingLogOutput_Should_RedirectLogOutput()
{
// arrange
var stringBuilder = new StringBuilder();
var logger = new TestStringLogger();
var client = new TestBaseClient(new RestClientOptions("")
{
LogWriters = new List<TextWriter> { new StringWriter(stringBuilder) }
LogWriters = new List<ILogger> { logger }
});
// act
client.Log(LogVerbosity.Info, "Test");
client.Log(LogLevel.Information, "Test");
// assert
Assert.IsFalse(string.IsNullOrEmpty(stringBuilder.ToString()));
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
}
[TestCase(LogVerbosity.None, LogVerbosity.Error, false)]
[TestCase(LogVerbosity.None, LogVerbosity.Warning, false)]
[TestCase(LogVerbosity.None, LogVerbosity.Info, false)]
[TestCase(LogVerbosity.None, LogVerbosity.Debug, false)]
[TestCase(LogVerbosity.Error, LogVerbosity.Error, true)]
[TestCase(LogVerbosity.Error, LogVerbosity.Warning, false)]
[TestCase(LogVerbosity.Error, LogVerbosity.Info, false)]
[TestCase(LogVerbosity.Error, LogVerbosity.Debug, false)]
[TestCase(LogVerbosity.Warning, LogVerbosity.Error, true)]
[TestCase(LogVerbosity.Warning, LogVerbosity.Warning, true)]
[TestCase(LogVerbosity.Warning, LogVerbosity.Info, false)]
[TestCase(LogVerbosity.Warning, LogVerbosity.Debug, false)]
[TestCase(LogVerbosity.Info, LogVerbosity.Error, true)]
[TestCase(LogVerbosity.Info, LogVerbosity.Warning, true)]
[TestCase(LogVerbosity.Info, LogVerbosity.Info, true)]
[TestCase(LogVerbosity.Info, LogVerbosity.Debug, false)]
[TestCase(LogVerbosity.Debug, LogVerbosity.Error, true)]
[TestCase(LogVerbosity.Debug, LogVerbosity.Warning, true)]
[TestCase(LogVerbosity.Debug, LogVerbosity.Info, true)]
[TestCase(LogVerbosity.Debug, LogVerbosity.Debug, true)]
public void SettingLogVerbosity_Should_RestrictLogging(LogVerbosity verbosity, LogVerbosity testVerbosity, bool expected)
[TestCase(LogLevel.None, LogLevel.Error, false)]
[TestCase(LogLevel.None, LogLevel.Warning, false)]
[TestCase(LogLevel.None, LogLevel.Information, false)]
[TestCase(LogLevel.None, LogLevel.Debug, false)]
[TestCase(LogLevel.Error, LogLevel.Error, true)]
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
[TestCase(LogLevel.Error, LogLevel.Information, false)]
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
[TestCase(LogLevel.Information, LogLevel.Error, true)]
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
[TestCase(LogLevel.Information, LogLevel.Information, true)]
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
[TestCase(null, LogLevel.Error, true)]
[TestCase(null, LogLevel.Warning, true)]
[TestCase(null, LogLevel.Information, true)]
[TestCase(null, LogLevel.Debug, true)]
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
{
// arrange
var stringBuilder = new StringBuilder();
var logger = new TestStringLogger();
var client = new TestBaseClient(new RestClientOptions("")
{
LogWriters = new List<TextWriter> { new StringWriter(stringBuilder) },
LogVerbosity = verbosity
LogWriters = new List<ILogger> { logger },
LogLevel = verbosity
});
// act
client.Log(testVerbosity, "Test");
// assert
Assert.AreEqual(!string.IsNullOrEmpty(stringBuilder.ToString()), expected);
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
}
[TestCase]

View File

@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0-preview-20180807-05"></packagereference>
<PackageReference Include="Moq" Version="4.9.0" />
<packagereference Include="NUnit" Version="3.10.1"></packagereference>
<packagereference Include="NUnit3TestAdapter" Version="3.10.0"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"></packagereference>
<PackageReference Include="Moq" Version="4.16.1" />
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
<packagereference Include="NUnit3TestAdapter" Version="3.17.0"></packagereference>
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,73 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using System.Globalization;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ExchangeHelpersTests
{
[TestCase(0.1, 1, 0.4, 0.4)]
[TestCase(0.1, 1, 0.1, 0.1)]
[TestCase(0.1, 1, 1, 1)]
[TestCase(0.1, 1, 0.99, 0.99)]
[TestCase(0.1, 1, 0.09, 0.1)]
[TestCase(0.1, 1, 1.01, 1)]
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
{
var result = ExchangeHelpers.ClampValue(min, max, input);
Assert.AreEqual(expected, result);
}
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.1, 0.1)]
[TestCase(0.1, 1, 0.1, RoundingType.Down, 1, 1)]
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.99, 0.9)]
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.09, 0.1)]
[TestCase(0.1, 1, 0.1, RoundingType.Down, 1.01, 1)]
[TestCase(0.1, 1, 0.01, RoundingType.Down, 0.532, 0.53)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
Assert.AreEqual(expected, result);
}
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.1, 0.1)]
[TestCase(0.1, 1, 2, RoundingType.Closest, 1, 1)]
[TestCase(0.1, 1, 2, RoundingType.Down, 0.555, 0.55)]
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.555, 0.56)]
[TestCase(0, 100, 5, RoundingType.Closest, 23.125987, 23.126)]
[TestCase(0, 100, 5, RoundingType.Down, 23.125987, 23.125)]
[TestCase(0, 100, 8, RoundingType.Down, 0.145647985948, 0.14564798)]
[TestCase(0, 100, 8, RoundingType.Closest, 0.145647985948, 0.14564799)]
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
Assert.AreEqual(expected, result);
}
[TestCase(5, 0.1563158, 0.15631)]
[TestCase(5, 12.1789258, 12.17892)]
[TestCase(2, 12.1789258, 12.17)]
[TestCase(8, 156146.1247, 156146.1247)]
[TestCase(8, 50, 50)]
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
{
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
Assert.AreEqual(expected, result);
}
[TestCase(0.1234560000, "0.123456")]
[TestCase(794.1230130600, "794.12301306")]
public void NormalizeTests(decimal input, string expected)
{
var result = ExchangeHelpers.Normalize(input);
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
}
}
}

View File

@ -1,5 +1,4 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Newtonsoft.Json;
@ -10,6 +9,7 @@ using System.Diagnostics;
using System.Linq;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.RateLimiter;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.UnitTests
{
@ -175,7 +175,7 @@ namespace CryptoExchange.Net.UnitTests
{
RateLimiters = new List<IRateLimiter> { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) },
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
LogVerbosity = LogVerbosity.Debug,
LogLevel = LogLevel.Debug,
ApiCredentials = new ApiCredentials("TestKey", "TestSecret")
});
client.SetResponse("{\"property\": 123}");

View File

@ -1,9 +1,9 @@
using System;
using System.Threading;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
@ -49,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
public void SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug });
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
@ -57,9 +57,9 @@ namespace CryptoExchange.Net.UnitTests
var sub = new SocketConnection(client, socket);
var rstEvent = new ManualResetEvent(false);
JToken result = null;
sub.AddHandler(SocketSubscription.CreateForIdentifier("TestHandler", true, (connection, data) =>
sub.AddSubscription(SocketSubscription.CreateForIdentifier("TestHandler", true, (messageEvent) =>
{
result = data;
result = messageEvent.JsonData;
rstEvent.Set();
}));
client.ConnectSocketSub(sub);
@ -71,13 +71,41 @@ namespace CryptoExchange.Net.UnitTests
// assert
Assert.IsTrue((int)result["property"] == 123);
}
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(client, socket);
var rstEvent = new ManualResetEvent(false);
string original = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier("TestHandler", true, (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
}));
client.ConnectSocketSub(sub);
// act
socket.InvokeMessage("{\"property\": 123}");
rstEvent.WaitOne(1000);
// assert
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, LogVerbosity = LogVerbosity.Debug });
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
@ -104,15 +132,15 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug });
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(client, socket);
client.ConnectSocketSub(sub);
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (d, a) => {}));
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (e) => {}));
// act
client.Unsubscribe(ups).Wait();
client.UnsubscribeAsync(ups).Wait();
// assert
Assert.IsTrue(socket.Connected == false);
@ -122,7 +150,7 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug });
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket1 = client.CreateSocket();
var socket2 = client.CreateSocket();
socket1.CanConnect = true;
@ -133,7 +161,7 @@ namespace CryptoExchange.Net.UnitTests
client.ConnectSocketSub(sub2);
// act
client.UnsubscribeAll().Wait();
client.UnsubscribeAllAsync().Wait();
// assert
Assert.IsTrue(socket1.Connected == false);
@ -144,7 +172,7 @@ namespace CryptoExchange.Net.UnitTests
public void FailingToConnectSocket_Should_ReturnError()
{
// arrange
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug });
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
var socket = client.CreateSocket();
socket.CanConnect = false;
var sub = new SocketConnection(client, socket);

View File

@ -1,12 +1,10 @@
using System;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.OrderBook;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
namespace CryptoExchange.Net.UnitTests
@ -24,15 +22,32 @@ namespace CryptoExchange.Net.UnitTests
public override void Dispose() {}
protected override Task<CallResult<bool>> DoResync()
protected override Task<CallResult<bool>> DoResyncAsync()
{
throw new NotImplementedException();
}
protected override Task<CallResult<UpdateSubscription>> DoStart()
protected override Task<CallResult<UpdateSubscription>> DoStartAsync()
{
throw new NotImplementedException();
}
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
Status = OrderBookStatus.Synced;
base.bids.Clear();
foreach (var bid in bids)
base.bids.Add(bid.Price, bid);
base.asks.Clear();
foreach (var ask in asks)
base.asks.Add(ask.Price, ask);
}
}
public class BookEntry : ISymbolOrderBookEntry
{
public decimal Quantity { get; set; }
public decimal Price { get; set; }
}
[TestCase]
@ -65,5 +80,33 @@ namespace CryptoExchange.Net.UnitTests
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
}
[TestCase]
public void CalculateAverageFillPrice()
{
var orderbook = new TestableSymbolOrderBook();
orderbook.SetData(new List<ISymbolOrderBookEntry>
{
new BookEntry{ Price = 1, Quantity = 1 },
new BookEntry{ Price = 1.1m, Quantity = 1 },
},
new List<ISymbolOrderBookEntry>()
{
new BookEntry{ Price = 1.2m, Quantity = 1 },
new BookEntry{ Price = 1.3m, Quantity = 1 },
});
var resultBids = orderbook.CalculateAverageFillPrice(2, OrderBookEntryType.Bid);
var resultAsks = orderbook.CalculateAverageFillPrice(2, OrderBookEntryType.Ask);
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
Assert.True(resultBids.Success);
Assert.True(resultAsks.Success);
Assert.AreEqual(1.05m, resultBids.Data);
Assert.AreEqual(1.25m, resultAsks.Data);
Assert.AreEqual(1.06666667m, resultBids2.Data);
Assert.AreEqual(1.23333333m, resultAsks2.Data);
}
}
}

View File

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.UnitTests
{
@ -17,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
{
}
public void Log(LogVerbosity verbosity, string data)
public void Log(LogLevel verbosity, string data)
{
log.Write(verbosity, data);
}

View File

@ -40,11 +40,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream));
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponse(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
var factory = Mock.Get(RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
@ -57,7 +57,7 @@ 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.GetResponse(It.IsAny<CancellationToken>())).Throws(we);
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>()))
@ -73,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream));
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponse(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
var factory = Mock.Get(RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
@ -86,7 +86,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
{
return await SendRequest<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
}
}

View File

@ -1,8 +1,9 @@
using System;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using WebSocket4Net;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@ -23,12 +24,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public Func<byte[], string> DataInterpreterBytes { get; set; }
public DateTime? DisconnectTime { get; set; }
public string Url { get; }
public WebSocketState SocketState { get; }
public bool IsClosed => !Connected;
public bool IsOpen => Connected;
public bool PingConnection { get; set; }
public TimeSpan PingInterval { get; set; }
public SslProtocols SSLProtocols { get; set; }
public Encoding Encoding { get; set; }
public int ConnectCalls { get; private set; }
public bool Reconnecting { get; set; }
@ -46,7 +47,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
}
public Task<bool> Connect()
public Task<bool> ConnectAsync()
{
Connected = CanConnect;
ConnectCalls++;
@ -65,7 +66,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
}
public Task Close()
public Task CloseAsync()
{
Connected = false;
DisconnectTime = DateTime.UtcNow;
@ -97,5 +98,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
OnMessage?.Invoke(data);
}
public void SetProxy(ApiProxy proxy)
{
throw new NotImplementedException();
}
}
}

View File

@ -29,7 +29,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
{
return ConnectSocket(sub).Result;
return ConnectSocketAsync(sub).Result;
}
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
@ -53,12 +53,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return true;
}
protected internal override Task<CallResult<bool>> AuthenticateSocket(SocketConnection s)
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
{
throw new NotImplementedException();
}
protected internal override Task<bool> Unsubscribe(SocketConnection connection, SocketSubscription s)
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
{
throw new NotImplementedException();
}

View File

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

View File

@ -36,18 +36,21 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Create Api credentials providing a api key and secret for authentication
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(SecureString key, SecureString secret)
{
if (key == null || secret == null)
throw new ArgumentException("Key and secret can't be null/empty");
Key = key;
Secret = secret;
}
/// <summary>
/// Create Api credentials providing a api key and secret for authentication
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>

View File

@ -24,15 +24,15 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Add authentication to the parameter list
/// Add authentication to the parameter list based on the provided credentials
/// </summary>
/// <param name="uri"></param>
/// <param name="method"></param>
/// <param name="parameters"></param>
/// <param name="signed"></param>
/// <param name="postParameterPosition"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
/// <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="postParameterPosition">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 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,
PostParameters postParameterPosition, ArrayParametersSerialization arraySerialization)
{
@ -40,15 +40,15 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Add authentication to the header dictionary
/// Add authentication to the header dictionary based on the provided credentials
/// </summary>
/// <param name="uri"></param>
/// <param name="method"></param>
/// <param name="parameters"></param>
/// <param name="signed"></param>
/// <param name="postParameterPosition"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
/// <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="postParameterPosition">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,
PostParameters postParameterPosition, ArrayParametersSerialization arraySerialization)
{
@ -80,9 +80,9 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected string ByteToString(byte[] buff)
protected static string ByteToString(byte[] buff)
{
var result = "";
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2"); /* hex format */
return result;

View File

@ -2,6 +2,7 @@
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;
@ -16,7 +17,7 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net
{
/// <summary>
/// The base for all clients
/// The base for all clients, websocket client and rest client
/// </summary>
public abstract class BaseClient : IDisposable
{
@ -25,9 +26,9 @@ namespace CryptoExchange.Net
/// </summary>
public string BaseAddress { get; }
/// <summary>
/// The name of the client
/// The name of the exchange the client is for
/// </summary>
public string ClientName { get; }
public string ExchangeName { get; }
/// <summary>
/// The log object
/// </summary>
@ -37,16 +38,19 @@ namespace CryptoExchange.Net
/// </summary>
protected ApiProxy? apiProxy;
/// <summary>
/// The auth provider
/// 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>
/// The last used id
/// 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>
@ -54,6 +58,9 @@ namespace CryptoExchange.Net
/// </summary>
protected static object idLock = new object();
/// <summary>
/// A default serializer
/// </summary>
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
@ -61,43 +68,44 @@ namespace CryptoExchange.Net
});
/// <summary>
/// Last is used
/// Last id used
/// </summary>
public static int LastId => lastId;
/// <summary>
/// ctor
/// </summary>
/// <param name="clientName"></param>
/// <param name="options"></param>
/// <param name="authenticationProvider"></param>
protected BaseClient(string clientName, ClientOptions options, AuthenticationProvider? authenticationProvider)
/// <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(clientName);
log = new Log(exchangeName);
authProvider = authenticationProvider;
log.UpdateWriters(options.LogWriters);
log.Level = options.LogVerbosity;
log.Level = options.LogLevel;
ClientName = clientName;
ExchangeName = exchangeName;
OutputOriginalData = options.OutputOriginalData;
BaseAddress = options.BaseAddress;
apiProxy = options.Proxy;
log.Write(LogVerbosity.Debug, $"Client configuration: {options}");
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
/// 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(LogVerbosity.Debug, "Setting api credentials");
log.Write(LogLevel.Debug, "Setting api credentials");
authProvider = authenticationProvider;
}
/// <summary>
/// Tries to parse the json data and returns a token
/// 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>
@ -106,7 +114,7 @@ namespace CryptoExchange.Net
if (string.IsNullOrEmpty(data))
{
var info = "Empty data object received";
log.Write(LogVerbosity.Error, info);
log.Write(LogLevel.Error, info);
return new CallResult<JToken>(null, new DeserializeError(info, data));
}
@ -126,7 +134,7 @@ namespace CryptoExchange.Net
}
catch (Exception ex)
{
var exceptionInfo = GetExceptionInfo(ex);
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<JToken>(null, new DeserializeError(info, data));
}
@ -139,14 +147,14 @@ namespace CryptoExchange.Net
/// <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</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(LogVerbosity.Error, tokenResult.Error!.Message);
log.Write(LogLevel.Error, tokenResult.Error!.Message);
return new CallResult<T>(default, tokenResult.Error);
}
@ -160,7 +168,7 @@ namespace CryptoExchange.Net
/// <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">A request identifier</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)
{
@ -169,8 +177,10 @@ namespace CryptoExchange.Net
try
{
if ((checkObject ?? ShouldCheckObjects)&& log.Level == LogVerbosity.Debug)
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)
@ -185,7 +195,7 @@ namespace CryptoExchange.Net
}
catch (Exception e)
{
log.Write(LogVerbosity.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message));
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message));
}
}
@ -194,20 +204,20 @@ namespace CryptoExchange.Net
catch (JsonReaderException jre)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
log.Write(LogVerbosity.Error, info);
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}";
log.Write(LogVerbosity.Error, info);
log.Write(LogLevel.Error, info);
return new CallResult<T>(default, new DeserializeError(info, obj));
}
catch (Exception ex)
{
var exceptionInfo = GetExceptionInfo(ex);
var exceptionInfo = ex.ToLogString();
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}";
log.Write(LogVerbosity.Error, info);
log.Write(LogLevel.Error, info);
return new CallResult<T>(default, new DeserializeError(info, obj));
}
}
@ -218,24 +228,33 @@ namespace CryptoExchange.Net
/// <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</param>
/// <param name="elapsedMilliseconds">Milliseconds response time</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>> Deserialize<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
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 (log.Level == LogVerbosity.Debug)
// 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(LogVerbosity.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}");
return Deserialize<T>(data, null, serializer, requestId);
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);
}
@ -244,12 +263,13 @@ namespace CryptoExchange.Net
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 ReadStream(stream).ConfigureAwait(false);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Debug LogVerbosity]";
log.Write(LogVerbosity.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
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)
@ -258,12 +278,12 @@ namespace CryptoExchange.Net
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStream(stream).ConfigureAwait(false);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Debug LogVerbosity]";
data = "[Data only available in Debug LogLevel]";
log.Write(LogVerbosity.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
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)
@ -271,18 +291,18 @@ namespace CryptoExchange.Net
string data;
if (stream.CanSeek) {
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStream(stream).ConfigureAwait(false);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
data = "[Data only available in Debug LogVerbosity]";
data = "[Data only available in Debug LogLevel]";
var exceptionInfo = GetExceptionInfo(ex);
log.Write(LogVerbosity.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
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> ReadStream(Stream stream)
private async Task<string> ReadStreamAsync(Stream stream)
{
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
return await reader.ReadToEndAsync().ConfigureAwait(false);
@ -302,7 +322,7 @@ namespace CryptoExchange.Net
if (!obj.HasValues && type != typeof(object))
{
log.Write(LogVerbosity.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty");
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty");
return;
}
@ -329,7 +349,7 @@ namespace CryptoExchange.Net
{
if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
{
log.Write(LogVerbosity.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
isDif = true;
}
continue;
@ -359,11 +379,11 @@ namespace CryptoExchange.Net
continue;
isDif = true;
log.Write(LogVerbosity.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
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(LogVerbosity.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj);
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj);
}
private static PropertyInfo? GetProperty(string name, IEnumerable<PropertyInfo> props)
@ -399,7 +419,7 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Generate a unique id
/// 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()
@ -431,37 +451,13 @@ namespace CryptoExchange.Net
return path;
}
/// <summary>
/// Get's all exception messages from a nested exception
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
public static string GetExceptionInfo(Exception ex)
{
string result = "";
var padding = 0;
while (true)
{
result += ex.Message.PadLeft(ex.Message.Length + padding) + Environment.NewLine;
if (ex.InnerException == null)
break;
ex = ex.InnerException;
padding += 2;
}
return result;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
authProvider?.Credentials?.Dispose();
log.Write(LogVerbosity.Debug, "Disposing exchange client");
log.Write(LogLevel.Debug, "Disposing exchange client");
}
}
}

View File

@ -11,7 +11,8 @@ using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Converter for arrays to properties
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
public class ArrayConverter : JsonConverter
{

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
@ -6,16 +6,16 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency exchange API's</Description>
<PackageVersion>3.9.0</PackageVersion>
<AssemblyVersion>3.9.0</AssemblyVersion>
<FileVersion>3.9.0</FileVersion>
<PackageVersion>4.0.0</PackageVersion>
<AssemblyVersion>4.0.0</AssemblyVersion>
<FileVersion>4.0.0</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>3.9.0 - Added optional JsonSerializer parameter to SendRequest to use during deserialization, Fix for unhandled message warning when unsubscribing a socket subscription</PackageReleaseNotes>
<PackageReleaseNotes>4.0.0</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -37,7 +37,15 @@
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="WebSocket4Net" Version="0.15.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,120 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net
{
/// <summary>
/// General helpers functions
/// </summary>
public static class ExchangeHelpers
{
/// <summary>
/// Clamp a value between a min and max
/// </summary>
/// <param name="min"></param>
/// <param name="max"></param>
/// <param name="value"></param>
/// <returns></returns>
public static decimal ClampValue(decimal min, decimal max, decimal value)
{
value = Math.Min(max, value);
value = Math.Max(min, value);
return value;
}
/// <summary>
/// Adjust a value to be between the min and max parameters and rounded to the closest step.
/// </summary>
/// <param name="min">The min value</param>
/// <param name="max">The max value</param>
/// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
/// <param name="roundingType">How to round</param>
/// <param name="value">The input value</param>
/// <returns></returns>
public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
{
if(step == 0)
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step));
value = Math.Min(max, value);
value = Math.Max(min, value);
if (step == null)
return value;
var offset = value % step.Value;
if(roundingType == RoundingType.Down)
value -= offset;
else
{
if (offset < step / 2)
value -= offset;
else value += (step.Value - offset);
}
value = RoundDown(value, 8);
return value.Normalize();
}
/// <summary>
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
/// </summary>
/// <param name="min">The min value</param>
/// <param name="max">The max value</param>
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
/// <param name="roundingType">How to round</param>
/// <param name="value">The input value</param>
/// <returns></returns>
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
{
value = Math.Min(max, value);
value = Math.Max(min, value);
if (precision == null)
return value;
return RoundToSignificantDigits(value, precision.Value, roundingType);
}
/// <summary>
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
/// </summary>
/// <param name="value">The value to round</param>
/// <param name="digits">The total amount of digits (NOT decimal places) to round to</param>
/// <param name="roundingType">How to round</param>
/// <returns></returns>
public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType)
{
var val = (double)value;
if (value == 0)
return 0;
double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1);
if(roundingType == RoundingType.Closest)
return (decimal)(scale * Math.Round(val / scale, digits));
else
return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
}
/// <summary>
/// Rounds a value down to
/// </summary>
/// <param name="i"></param>
/// <param name="decimalPlaces"></param>
/// <returns></returns>
public static decimal RoundDown(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power;
}
/// <summary>
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static decimal Normalize(this decimal value)
{
return value / 1.000000000000000000000000000000000m;
}
}
}

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common balance

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
{
@ -29,5 +27,9 @@ namespace CryptoExchange.Net.ExchangeInterfaces
/// The asset fee was paid in
/// </summary>
public string? CommonFeeAsset { get; }
/// <summary>
/// Trade time
/// </summary>
DateTime CommonTradeTime { get; }
}
}

View File

@ -10,6 +10,15 @@ namespace CryptoExchange.Net.ExchangeInterfaces
/// </summary>
public interface IExchangeClient
{
/// <summary>
/// Should be triggered on order placing
/// </summary>
event Action<ICommonOrderId> OnOrderPlaced;
/// <summary>
/// Should be triggered on order cancelling
/// </summary>
event Action<ICommonOrderId> OnOrderCanceled;
/// <summary>
/// Get the symbol name based on a base and quote asset
/// </summary>
@ -146,5 +155,23 @@ namespace CryptoExchange.Net.ExchangeInterfaces
/// </summary>
Sell
}
/// <summary>
/// Common order status
/// </summary>
public enum OrderStatus
{
/// <summary>
/// placed and not fully filled order
/// </summary>
Active,
/// <summary>
/// cancelled order
/// </summary>
Canceled,
/// <summary>
/// filled order
/// </summary>
Filled
}
}
}

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
{

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
{
@ -24,7 +22,7 @@ namespace CryptoExchange.Net.ExchangeInterfaces
/// <summary>
/// Status of the order
/// </summary>
public string CommonStatus { get; }
public IExchangeClient.OrderStatus CommonStatus { get; }
/// <summary>
/// Whether the order is active
/// </summary>
@ -37,5 +35,9 @@ namespace CryptoExchange.Net.ExchangeInterfaces
/// Type of the order
/// </summary>
public IExchangeClient.OrderType CommonType { get; }
/// <summary>
/// order time
/// </summary>
DateTime CommonOrderTime { get; }
}
}

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.ExchangeInterfaces

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common order id

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
{

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common symbol

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.ExchangeInterfaces
namespace CryptoExchange.Net.ExchangeInterfaces
{
/// <summary>
/// Common ticker

View File

@ -2,14 +2,14 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
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;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -125,7 +125,7 @@ namespace CryptoExchange.Net
/// <returns></returns>
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
{
var uriString = "";
var uriString = string.Empty;
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
foreach (var arrayEntry in arraysParameters)
{
@ -252,14 +252,14 @@ namespace CryptoExchange.Net
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
log?.Write(LogVerbosity.Error, info);
log?.Write(LogLevel.Error, info);
if (log == null) Debug.WriteLine(info);
return null;
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
log?.Write(LogVerbosity.Error, info);
log?.Write(LogLevel.Error, info);
if (log == null) Debug.WriteLine(info);
return null;
}
@ -335,6 +335,33 @@ namespace CryptoExchange.Net
if (value == null || !value.Any())
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
}
/// <summary>
/// Format an exception and inner exception to a readable string
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public static string ToLogString(this Exception exception)
{
var message = new StringBuilder();
var indent = 0;
while (exception != null)
{
for (var i = 0; i < indent; i++)
message.Append(' ');
message.Append(exception.GetType().Name);
message.Append(" - ");
message.AppendLine(exception.Message);
for (var i = 0; i < indent; i++)
message.Append(' ');
message.AppendLine(exception.StackTrace);
indent += 2;
exception = exception.InnerException;
}
return message.ToString();
}
}
}

View File

@ -53,6 +53,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IResponse> GetResponse(CancellationToken cancellationToken);
Task<IResponse> GetResponseAsync(CancellationToken cancellationToken);
}
}

View File

@ -29,7 +29,7 @@ namespace CryptoExchange.Net.Interfaces
/// Get the response stream
/// </summary>
/// <returns></returns>
Task<Stream> GetResponseStream();
Task<Stream> GetResponseStreamAsync();
/// <summary>
/// Close the response

View File

@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Client name
/// </summary>
string ClientName { get; }
string ExchangeName { get; }
/// <summary>
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.

View File

@ -49,12 +49,12 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
Task Unsubscribe(UpdateSubscription subscription);
Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
Task UnsubscribeAll();
Task UnsubscribeAllAsync();
}
}

View File

@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
int BidCount { get; }
/// <summary>
/// Get a snapshot of the book at this moment
/// </summary>
(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book { get; }
/// <summary>
/// The list of asks
/// </summary>
@ -75,12 +80,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
(ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { get; }
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>
/// <returns></returns>
CallResult<bool> Start();
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>
@ -91,12 +90,15 @@ namespace CryptoExchange.Net.Interfaces
/// Stop syncing the order book
/// </summary>
/// <returns></returns>
void Stop();
Task StopAsync();
/// <summary>
/// Stop syncing the order book
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
/// at that price since between this calculation and the order placement the book can have changed.
/// </summary>
/// <returns></returns>
Task StopAsync();
/// <param name="quantity">The quantity in base asset to fill</param>
/// <param name="type">The type</param>
/// <returns>Average fill price</returns>
CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type);
}
}

View File

@ -1,7 +1,8 @@
using System;
using CryptoExchange.Net.Objects;
using System;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using WebSocket4Net;
namespace CryptoExchange.Net.Interfaces
{
@ -36,6 +37,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
string? Origin { get; set; }
/// <summary>
/// Encoding to use
/// </summary>
Encoding? Encoding { get; set; }
/// <summary>
/// Reconnecting
/// </summary>
bool Reconnecting { get; set; }
@ -52,10 +57,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
string Url { get; }
/// <summary>
/// State
/// </summary>
WebSocketState SocketState { get; }
/// <summary>
/// Is closed
/// </summary>
bool IsClosed { get; }
@ -75,7 +76,7 @@ namespace CryptoExchange.Net.Interfaces
/// Connect the socket
/// </summary>
/// <returns></returns>
Task<bool> Connect();
Task<bool> ConnectAsync();
/// <summary>
/// Send data
/// </summary>
@ -89,12 +90,11 @@ namespace CryptoExchange.Net.Interfaces
/// Close the connecting
/// </summary>
/// <returns></returns>
Task Close();
Task CloseAsync();
/// <summary>
/// Set proxy
/// </summary>
/// <param name="host"></param>
/// <param name="port"></param>
void SetProxy(string host, int port);
/// <param name="proxy"></param>
void SetProxy(ApiProxy proxy);
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
using System;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Log to console
/// </summary>
public class ConsoleLogger : ILogger
{
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state) => null!;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
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

@ -0,0 +1,25 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Default log writer, writes to debug
/// </summary>
public class DebugLogger: ILogger
{
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state) => null!;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
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

@ -1,21 +0,0 @@
using System.Diagnostics;
using System.IO;
using System.Text;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Default log writer, writes to debug
/// </summary>
public class DebugTextWriter: TextWriter
{
/// <inheritdoc />
public override Encoding Encoding => Encoding.ASCII;
/// <inheritdoc />
public override void WriteLine(string value)
{
Debug.WriteLine(value);
}
}
}

View File

@ -1,7 +1,7 @@
using System;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace CryptoExchange.Net.Logging
@ -11,11 +11,15 @@ namespace CryptoExchange.Net.Logging
/// </summary>
public class Log
{
private List<TextWriter> writers;
/// <summary>
/// The verbosity of the logging
/// List of ILogger implementations to forward the message to
/// </summary>
public LogVerbosity Level { get; set; } = LogVerbosity.Info;
private List<ILogger> writers;
/// <summary>
/// The verbosity of the logging, anything more verbose will not be forwarded to the writers
/// </summary>
public LogLevel? Level { get; set; } = LogLevel.Information;
/// <summary>
/// Client name
@ -25,17 +29,18 @@ namespace CryptoExchange.Net.Logging
/// <summary>
/// ctor
/// </summary>
/// <param name="clientName">The name of the client the logging is used in</param>
public Log(string clientName)
{
ClientName = clientName;
writers = new List<TextWriter>();
writers = new List<ILogger>();
}
/// <summary>
/// Set the writers
/// </summary>
/// <param name="textWriters"></param>
public void UpdateWriters(List<TextWriter> textWriters)
public void UpdateWriters(List<ILogger> textWriters)
{
writers = textWriters;
}
@ -43,52 +48,26 @@ namespace CryptoExchange.Net.Logging
/// <summary>
/// Write a log entry
/// </summary>
/// <param name="logType"></param>
/// <param name="message"></param>
public void Write(LogVerbosity logType, string message)
/// <param name="logLevel">The verbosity of the message</param>
/// <param name="message">The message to log</param>
public void Write(LogLevel logLevel, string message)
{
if ((int)logType < (int)Level)
if (Level != null && (int)logLevel < (int)Level)
return;
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {ClientName.PadRight(10)} | {logType} | {message}";
var logMessage = $"{ClientName,-10} | {message}";
foreach (var writer in writers.ToList())
{
try
{
writer.WriteLine(logMessage);
writer.Log(logLevel, logMessage);
}
catch (Exception e)
{
Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + (e.InnerException?.Message ?? e.Message));
// Can't write to the logging so where else to output..
Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
}
}
}
}
/// <summary>
/// The log verbosity
/// </summary>
public enum LogVerbosity
{
/// <summary>
/// Debug logging
/// </summary>
Debug,
/// <summary>
/// Info logging
/// </summary>
Info,
/// <summary>
/// Warning logging
/// </summary>
Warning,
/// <summary>
/// Error logging
/// </summary>
Error,
/// <summary>
/// None, used for disabling logging
/// </summary>
None
}
}

View File

@ -1,56 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// File writer
/// </summary>
public class ThreadSafeFileWriter: TextWriter
{
private static readonly object openedFilesLock = new object();
private static readonly List<string> openedFiles = new List<string>();
private readonly StreamWriter logWriter;
private readonly object writeLock;
/// <inheritdoc />
public override Encoding Encoding => Encoding.ASCII;
/// <summary>
/// ctor
/// </summary>
/// <param name="path"></param>
public ThreadSafeFileWriter(string path)
{
logWriter = new StreamWriter(File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) {AutoFlush = true};
writeLock = new object();
lock(openedFilesLock)
{
if (openedFiles.Contains(path))
throw new System.Exception("Can't have multiple ThreadSafeFileWriters for the same file, reuse a single instance");
openedFiles.Add(path);
}
}
/// <inheritdoc />
public override void WriteLine(string logMessage)
{
lock(writeLock)
logWriter.WriteLine(logMessage);
}
/// <summary>
/// Dispose
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
lock (writeLock)
logWriter.Close();
}
}
}

View File

@ -10,9 +10,10 @@ namespace CryptoExchange.Net.Objects
public class CallResult
{
/// <summary>
/// An error if the call didn't succeed
/// An error if the call didn't succeed, will always be filled if Success = false
/// </summary>
public Error? Error { get; internal set; }
/// <summary>
/// Whether the call was successful
/// </summary>
@ -54,20 +55,27 @@ namespace CryptoExchange.Net.Objects
public class CallResult<T>: CallResult
{
/// <summary>
/// The data returned by the call
/// The data returned by the call, only available when Success = true
/// </summary>
public T Data { get; internal set; }
/// <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; }
/// <summary>
/// ctor
/// </summary>
/// <param name="data"></param>
/// <param name="error"></param>
#pragma warning disable 8618
public CallResult([AllowNull]T data, Error? error): base(error)
#pragma warning restore 8618
{
#pragma warning disable 8601
Data = data;
#pragma warning disable 8601
#pragma warning restore 8601
}
/// <summary>
@ -132,9 +140,9 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="error"></param>
/// <param name="code">Status code</param>
/// <param name="responseHeaders">Response headers</param>
/// <param name="error">Error</param>
public WebCallResult(
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error? error) : base(error)
@ -146,9 +154,9 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// Create an error result
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="error"></param>
/// <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)
{
@ -191,31 +199,43 @@ namespace CryptoExchange.Net.Objects
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error)
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
[AllowNull] T data,
Error? error): base(data, error)
{
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
}
/// <summary>
/// Create new based on existing
/// ctor
/// </summary>
/// <param name="callResult"></param>
public WebCallResult(WebCallResult<T> callResult): base(callResult.Data, callResult.Error)
/// <param name="code"></param>
/// <param name="originalData"></param>
/// <param name="responseHeaders"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
string? originalData,
[AllowNull] T data,
Error? error) : base(data, error)
{
ResponseHeaders = callResult.ResponseHeaders;
ResponseStatusCode = callResult.ResponseStatusCode;
OriginalData = originalData;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
}
/// <summary>
/// Create from a call result
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="Y"></typeparam>
/// <param name="source"></param>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public static WebCallResult<T> CreateFrom<Y>(WebCallResult<Y> source) where Y : T
public WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<T>(source.ResponseStatusCode, source.ResponseHeaders, (T)source.Data, source.Error);
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, OriginalData, data, Error);
}
/// <summary>

View File

@ -16,7 +16,7 @@
}
/// <summary>
/// Where the post parameters should be added
/// Where the parameters for a post request should be added
/// </summary>
public enum PostParameters
{
@ -101,4 +101,19 @@
/// </summary>
Array
}
/// <summary>
/// How to round
/// </summary>
public enum RoundingType
{
/// <summary>
/// Round down (flooring)
/// </summary>
Down,
/// <summary>
/// Round to closest value
/// </summary>
Closest
}
}

View File

@ -55,7 +55,7 @@
}
/// <summary>
/// No api credentials provided while trying to access private endpoint
/// No api credentials provided while trying to access a private endpoint
/// </summary>
public class NoApiCredentialsError : Error
{
@ -169,4 +169,16 @@
/// </summary>
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
}
/// <summary>
/// Invalid operation requested
/// </summary>
public class InvalidOperationError: Error
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public InvalidOperationError(string message) : base(null, message, null) { }
}
}

View File

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects
{
@ -14,19 +14,24 @@ namespace CryptoExchange.Net.Objects
public class BaseOptions
{
/// <summary>
/// The log verbosity
/// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers.
/// </summary>
public LogVerbosity LogVerbosity { get; set; } = LogVerbosity.Info;
public LogLevel? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information;
/// <summary>
/// The log writers
/// </summary>
public List<TextWriter> LogWriters { get; set; } = new List<TextWriter> { new DebugTextWriter() };
public List<ILogger> LogWriters { get; set; } = new List<ILogger> { new DebugLogger() };
/// <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;
/// <inheritdoc />
public override string ToString()
{
return $"LogVerbosity: {LogVerbosity}, Writers: {LogWriters.Count}";
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, OutputOriginalData: {OutputOriginalData}";
}
}
@ -47,11 +52,12 @@ namespace CryptoExchange.Net.Objects
/// <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 is added which makes the total amount of bids 11, should the last bid entry be removed
/// 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>
@ -111,7 +117,7 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
/// <param name="baseAddress"></param>
/// <param name="baseAddress">The base address to use</param>
#pragma warning disable 8618
public ClientOptions(string baseAddress)
#pragma warning restore 8618
@ -122,7 +128,7 @@ namespace CryptoExchange.Net.Objects
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-": "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null? "-": Proxy.Host)}";
return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
}
}
@ -147,7 +153,7 @@ namespace CryptoExchange.Net.Objects
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 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 will be ignored in requests and should be set on the provided HttpClient instance
/// </summary>
public HttpClient? HttpClient { get; set; }
@ -177,7 +183,7 @@ namespace CryptoExchange.Net.Objects
var copy = new T
{
BaseAddress = BaseAddress,
LogVerbosity = LogVerbosity,
LogLevel = LogLevel,
Proxy = Proxy,
LogWriters = LogWriters,
RateLimiters = RateLimiters,
@ -215,24 +221,25 @@ namespace CryptoExchange.Net.Objects
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// The time to wait for a socket response
/// The time to wait for a socket response 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
/// 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.
/// </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.
/// Setting this to a higher number increases subscription speed, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection.
/// 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>
/// ctor
/// </summary>
/// <param name="baseAddress"></param>
/// <param name="baseAddress">The base address to use</param>
public SocketClientOptions(string baseAddress) : base(baseAddress)
{
}
@ -247,7 +254,7 @@ namespace CryptoExchange.Net.Objects
var copy = new T
{
BaseAddress = BaseAddress,
LogVerbosity = LogVerbosity,
LogLevel = LogLevel,
Proxy = Proxy,
LogWriters = LogWriters,
AutoReconnect = AutoReconnect,

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.OrderBook
@ -19,10 +20,10 @@ namespace CryptoExchange.Net.OrderBook
/// <summary>
/// List of asks
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
/// <summary>
/// List of bids
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}
}

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.OrderBook
@ -7,16 +8,16 @@ namespace CryptoExchange.Net.OrderBook
{
public long StartUpdateId { get; set; }
public long EndUpdateId { get; set; }
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}
internal class InitialOrderBookItem
{
public long StartUpdateId { get; set; }
public long EndUpdateId { get; set; }
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}
internal class ChecksumItem

View File

@ -2,7 +2,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -10,6 +9,7 @@ using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.OrderBook
{
@ -74,7 +74,7 @@ namespace CryptoExchange.Net.OrderBook
var old = status;
status = value;
log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} status changed: {old} => {value}");
log.Write(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
OnStatusChange?.Invoke(old, status);
}
}
@ -140,6 +140,18 @@ namespace CryptoExchange.Net.OrderBook
}
}
/// <summary>
/// Get a snapshot of the book at this moment
/// </summary>
public (IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book
{
get
{
lock (bookLock)
return (Bids, Asks);
}
}
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
{
public decimal Quantity { get { return 0m; } set {; } }
@ -208,51 +220,82 @@ namespace CryptoExchange.Net.OrderBook
asks = new SortedList<decimal, ISymbolOrderBookEntry>();
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
log = new Log(options.OrderBookName) { Level = options.LogVerbosity };
var writers = options.LogWriters ?? new List<TextWriter> { new DebugTextWriter() };
log = new Log(options.OrderBookName) { Level = options.LogLevel };
var writers = options.LogWriters ?? new List<ILogger> { new DebugLogger() };
log.UpdateWriters(writers.ToList());
}
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>
/// <returns></returns>
public CallResult<bool> Start() => StartAsync().Result;
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>
/// <returns></returns>
public async Task<CallResult<bool>> StartAsync()
{
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} starting");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} starting");
Status = OrderBookStatus.Connecting;
_processTask = Task.Run(ProcessQueue);
var startResult = await DoStart().ConfigureAwait(false);
var startResult = await DoStartAsync().ConfigureAwait(false);
if (!startResult)
return new CallResult<bool>(false, startResult.Error);
subscription = startResult.Data;
subscription.ConnectionLost += Reset;
subscription.ConnectionRestored += time => Resync();
subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false);
Status = OrderBookStatus.Synced;
return new CallResult<bool>(true, null);
}
/// <summary>
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
/// at that price since between this calculation and the order placement the book can have changed.
/// </summary>
/// <param name="quantity">The quantity in base asset to fill</param>
/// <param name="type">The type</param>
/// <returns>Average fill price</returns>
public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(0, new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
var totalCost = 0m;
var totalAmount = 0m;
var amountLeft = quantity;
lock (bookLock)
{
var list = type == OrderBookEntryType.Ask ? asks : bids;
var step = 0;
while (amountLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(0, new InvalidOperationError($"Quantity is larger than order in the order book"));
var element = list.ElementAt(step);
var stepAmount = Math.Min(element.Value.Quantity, amountLeft);
totalCost += stepAmount * element.Value.Price;
totalAmount += stepAmount;
amountLeft -= stepAmount;
step++;
}
}
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8), null);
}
private void Reset()
{
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost");
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
Status = OrderBookStatus.Reconnecting;
_queueEvent.Set();
// Clear queue
while(_processQueue.TryDequeue(out _))
while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear();
bookSet = false;
DoReset();
}
private void Resync()
private async Task ResyncAsync()
{
Status = OrderBookStatus.Syncing;
var success = false;
@ -261,39 +304,35 @@ namespace CryptoExchange.Net.OrderBook
if (Status != OrderBookStatus.Syncing)
return;
var resyncResult = DoResync().Result;
var resyncResult = await DoResyncAsync().ConfigureAwait(false);
success = resyncResult;
}
log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} successfully resynchronized");
log.Write(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
Status = OrderBookStatus.Synced;
}
/// <summary>
/// Stop syncing the order book
/// </summary>
/// <returns></returns>
public void Stop() => StopAsync().Wait();
/// <summary>
/// Stop syncing the order book
/// </summary>
/// <returns></returns>
public async Task StopAsync()
{
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} stopping");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
Status = OrderBookStatus.Disconnected;
_queueEvent.Set();
_processTask?.Wait();
if(_processTask != null)
await _processTask.ConfigureAwait(false);
if(subscription != null)
await subscription.Close().ConfigureAwait(false);
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Start the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<UpdateSubscription>> DoStart();
protected abstract Task<CallResult<UpdateSubscription>> DoStartAsync();
/// <summary>
/// Reset the order book
@ -304,7 +343,7 @@ namespace CryptoExchange.Net.OrderBook
/// Resync the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<bool>> DoResync();
protected abstract Task<CallResult<bool>> DoResyncAsync();
/// <summary>
/// Validate a checksum with the current order book
@ -341,6 +380,7 @@ namespace CryptoExchange.Net.OrderBook
if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected)
return;
bookSet = true;
asks.Clear();
foreach (var ask in item.Asks)
asks.Add(ask.Price, ask);
@ -354,7 +394,7 @@ namespace CryptoExchange.Net.OrderBook
BidCount = bids.Count;
LastOrderBookUpdate = DateTime.UtcNow;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
CheckProcessBuffer();
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
@ -377,7 +417,7 @@ namespace CryptoExchange.Net.OrderBook
FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId,
});
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
}
else
{
@ -385,12 +425,15 @@ namespace CryptoExchange.Net.OrderBook
var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
if (!asks.Any() || !bids.Any())
return;
if (asks.First().Key < bids.First().Key)
{
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} detected out of sync order book. Resyncing");
_ = subscription?.Reconnect();
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. Resyncing");
_ = subscription?.ReconnectAsync();
return;
}
}
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
CheckBestOffersChanged(prevBestBid, prevBestAsk);
@ -417,8 +460,8 @@ namespace CryptoExchange.Net.OrderBook
if(!checksumResult)
{
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_ = subscription?.Reconnect();
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_ = subscription?.ReconnectAsync();
return;
}
}
@ -432,8 +475,6 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="bidList">List of bids</param>
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> askList)
{
bookSet = true;
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
_queueEvent.Set();
}
@ -489,9 +530,9 @@ namespace CryptoExchange.Net.OrderBook
private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
if (lastUpdateId < LastSequenceNumber)
if (lastUpdateId <= LastSequenceNumber)
{
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
return;
}
@ -503,13 +544,13 @@ namespace CryptoExchange.Net.OrderBook
if (Levels.HasValue && strictLevels)
{
while (this.bids.Count() > Levels.Value)
while (this.bids.Count > Levels.Value)
{
BidCount--;
this.bids.Remove(this.bids.Last().Key);
}
while (this.asks.Count() > Levels.Value)
while (this.asks.Count > Levels.Value)
{
AskCount--;
this.asks.Remove(this.asks.Last().Key);
@ -517,7 +558,7 @@ namespace CryptoExchange.Net.OrderBook
}
LastSequenceNumber = lastUpdateId;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
}
/// <summary>
@ -527,7 +568,7 @@ namespace CryptoExchange.Net.OrderBook
{
var pbList = processBuffer.ToList();
if(pbList.Count > 0)
log.Write(LogVerbosity.Debug, "Processing buffered updates");
log.Write(LogLevel.Debug, "Processing buffered updates");
foreach (var bufferEntry in pbList)
{
@ -549,15 +590,15 @@ namespace CryptoExchange.Net.OrderBook
if (sequence <= LastSequenceNumber)
{
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{sequence}");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}");
return false;
}
if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
{
// Out of sync
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
subscription?.Reconnect();
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
subscription?.ReconnectAsync();
return false;
}
@ -594,7 +635,7 @@ namespace CryptoExchange.Net.OrderBook
/// </summary>
/// <param name="timeout">Max wait time</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitForSetOrderBook(int timeout)
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(int timeout)
{
var startWait = DateTime.UtcNow;
while (!bookSet && Status == OrderBookStatus.Syncing)
@ -636,7 +677,7 @@ namespace CryptoExchange.Net.OrderBook
/// <returns></returns>
public string ToString(int numberOfEntries)
{
var result = "";
var result = string.Empty;
result += $"Asks ({AskCount}): {Environment.NewLine}";
foreach (var entry in Asks.Take(numberOfEntries).Reverse())
result += $" {entry.Price.ToString(CultureInfo.InvariantCulture).PadLeft(8)} | {entry.Quantity.ToString(CultureInfo.InvariantCulture).PadRight(8)}{Environment.NewLine}";

View File

@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
@ -71,7 +70,7 @@ namespace CryptoExchange.Net.Requests
}
/// <inheritdoc />
public async Task<IResponse> GetResponse(CancellationToken cancellationToken)
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
{
return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
}

View File

@ -33,7 +33,7 @@ namespace CryptoExchange.Net.Requests
}
/// <inheritdoc />
public async Task<Stream> GetResponseStream()
public async Task<Stream> GetResponseStreamAsync()
{
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}

View File

@ -7,16 +7,15 @@ using System.Linq;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiter;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -33,9 +32,10 @@ namespace CryptoExchange.Net
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <summary>
/// Where to place post parameters
/// Where to place post parameters by default
/// </summary>
protected PostParameters postParametersPosition = PostParameters.InBody;
/// <summary>
/// Request body content type
/// </summary>
@ -47,21 +47,21 @@ namespace CryptoExchange.Net
protected bool manualParseError = false;
/// <summary>
/// How to serialize array parameters
/// How to serialize array parameters when making requests
/// </summary>
protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be when no data is send
/// 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
/// 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>
/// Rate limiting behaviour
/// What should happen when running into a rate limit
/// </summary>
public RateLimitingBehaviour RateLimitBehaviour { get; }
/// <summary>
@ -69,17 +69,17 @@ namespace CryptoExchange.Net
/// </summary>
public IEnumerable<IRateLimiter> RateLimiters { get; private set; }
/// <summary>
/// Total requests made
/// Total requests made by this client
/// </summary>
public int TotalRequestsMade { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="clientName"></param>
/// <param name="exchangeOptions"></param>
/// <param name="authenticationProvider"></param>
protected RestClient(string clientName, RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(clientName, exchangeOptions, authenticationProvider)
/// <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));
@ -158,31 +158,31 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Execute a request
/// Execute a request to the uri and deserialize the response into the provided type parameter
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <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="postPosition">Where the post parameters should be placed</param>
/// <param name="arraySerialization">How array parameters should be serialized</param>
/// <param name="postPosition">Where the post 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>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequest<T>(Uri uri, HttpMethod method, CancellationToken cancellationToken,
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,
PostParameters? postPosition = null, ArrayParametersSerialization? arraySerialization = null, int credits = 1,
JsonSerializer? deserializer = null) where T : class
{
var requestId = NextId();
log.Write(LogVerbosity.Debug, $"[{requestId}] Creating request for " + uri);
log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri);
if (signed && authProvider == null)
{
log.Write(LogVerbosity.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new WebCallResult<T>(null, null, null, new NoApiCredentialsError());
}
@ -192,74 +192,82 @@ namespace CryptoExchange.Net
var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour, credits);
if (!limitResult.Success)
{
log.Write(LogVerbosity.Debug, $"[{requestId}] Request {uri.AbsolutePath} failed because of rate limit");
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(LogVerbosity.Debug, $"[{requestId}] Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}");
log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}");
}
string? paramString = null;
if (method == HttpMethod.Post)
paramString = " with request body " + request.Content;
log.Write(LogVerbosity.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")}");
return await GetResponse<T>(request, deserializer, cancellationToken).ConfigureAwait(false);
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 string result
/// 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>> GetResponse<T>(IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken)
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken)
{
try
{
TotalRequestsMade++;
var sw = Stopwatch.StartNew();
var response = await request.GetResponse(cancellationToken).ConfigureAwait(false);
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseStream = await response.GetResponseStream().ConfigureAwait(false);
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(LogVerbosity.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms: {data}");
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!);
var error = await TryParseError(parseResult.Data);
// 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, deserializeResult.Data, deserializeResult.Error);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, OutputOriginalData ? data: null, deserializeResult.Data, deserializeResult.Error);
}
else
{
var desResult = await Deserialize<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
// 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, desResult.Data, desResult.Error);
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(LogVerbosity.Debug, $"[{request.RequestId}] Error received: {data}");
log.Write(LogLevel.Debug, $"[{request.RequestId}] Error received: {data}");
responseStream.Close();
response.Close();
var parseResult = ValidateJson(data);
@ -271,22 +279,23 @@ namespace CryptoExchange.Net
}
catch (HttpRequestException requestException)
{
var exceptionInfo = GetExceptionInfo(requestException);
log.Write(LogVerbosity.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
// 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 (TaskCanceledException canceledException)
{
if (canceledException.CancellationToken == cancellationToken)
{
// Cancellation token cancelled
log.Write(LogVerbosity.Warning, $"[{request.RequestId}] Request cancel requested");
// 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(LogVerbosity.Warning, $"[{request.RequestId}] 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"));
}
}
@ -294,11 +303,12 @@ namespace CryptoExchange.Net
/// <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.
/// This can be used together with ManualParseError to check if it is an error before deserializing to an object
/// 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?> TryParseError(JToken data)
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
{
return Task.FromResult<ServerError?>(null);
}
@ -349,20 +359,22 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Writes the parameters of the request to the request object, either in the query string or the request body
/// Writes the parameters of the request to the request object body
/// </summary>
/// <param name="request"></param>
/// <param name="parameters"></param>
/// <param name="contentType"></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(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))
{

View File

@ -7,9 +7,9 @@ using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -31,6 +31,7 @@ namespace CryptoExchange.Net
/// </summary>
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new ConcurrentDictionary<int, SocketConnection>();
/// <summary>
/// Semaphore used while creating sockets
/// </summary>
protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
@ -48,29 +49,28 @@ namespace CryptoExchange.Net
public int MaxSocketConnections { get; protected set; } = 9999;
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
public int SocketCombineTarget { get; protected set; }
/// <summary>
/// Handler for byte data
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
protected Func<byte[], string>? dataInterpreterBytes;
/// <summary>
/// Handler for string data
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// </summary>
protected Func<string, string>? dataInterpreterString;
/// <summary>
/// Generic handlers
/// 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<SocketConnection, JToken>> genericHandlers = new Dictionary<string, Action<SocketConnection, JToken>>();
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new Dictionary<string, Action<MessageEvent>>();
/// <summary>
/// Periodic task
/// 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>
protected Task? periodicTask;
/// <summary>
/// Periodic task event
/// Wait event for the periodicTask
/// </summary>
protected AutoResetEvent? periodicEvent;
/// <summary>
/// Is disposing
/// If client is disposing
/// </summary>
protected bool disposing;
@ -87,12 +87,12 @@ namespace CryptoExchange.Net
#endregion
/// <summary>
/// Create a socket client
/// ctor
/// </summary>
/// <param name="clientName">Client name</param>
/// <param name="exchangeOptions">Client options</param>
/// <param name="authenticationProvider">Authentication provider</param>
protected SocketClient(string clientName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(clientName, exchangeOptions, authenticationProvider)
/// <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)
{
if (exchangeOptions == null)
throw new ArgumentNullException(nameof(exchangeOptions));
@ -105,7 +105,7 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Set a function to interpret the data, used when the data is received as bytes instead of a string
/// Set a delegate to be used for processing data received from socket connections before it is processed by handlers
/// </summary>
/// <param name="byteHandler">Handler for byte data</param>
/// <param name="stringHandler">Handler for string data</param>
@ -116,98 +116,103 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Subscribe
/// Connect to an url and listen for data on the BaseAddress
/// </summary>
/// <typeparam name="T">The expected return data</typeparam>
/// <param name="request">The request to send</param>
/// <param name="identifier">The identifier to use</param>
/// <param name="authenticated">If the subscription should be authenticated</param>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <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>
/// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> Subscribe<T>(object? request, string? identifier, bool authenticated, Action<T> dataHandler)
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
{
return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler);
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler);
}
/// <summary>
/// Subscribe using a specif URL
/// Connect to an url and listen for data
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="url">The URL to connect to</param>
/// <param name="request">The request to send</param>
/// <param name="identifier">The identifier to use</param>
/// <param name="authenticated">If the subscription should be authenticated</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>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> Subscribe<T>(string url, object? request, string? identifier, bool authenticated, Action<T> dataHandler)
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
{
SocketConnection socket;
SocketSubscription handler;
SocketConnection socketConnection;
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
{
socket = GetWebsocket(url, authenticated);
handler = AddHandler(request, identifier, true, socket, dataHandler);
// Get a new or existing socket connection
socketConnection = GetSocketConnection(url, authenticated);
// Add a subscription on the socket connection
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
if (SocketCombineTarget == 1)
{
// Can release early when only a single sub per connection
// 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 = !socket.Connected;
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(null, connectResult.Error);
if (needsconnecting)
log.Write(LogVerbosity.Debug, $"Socket {socket.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}");
if (needsConnecting)
log.Write(LogLevel.Debug, $"Socket {socketConnection.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}");
}
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 (socket.PausedActivity)
if (socketConnection.PausedActivity)
{
log.Write(LogVerbosity.Info, "Socket has been paused, can't subscribe at this moment");
log.Write(LogLevel.Information, "Socket has been paused, can't subscribe at this moment");
return new CallResult<UpdateSubscription>(default, new ServerError("Socket is paused"));
}
if (request != null)
{
var subResult = await SubscribeAndWait(socket, request, handler).ConfigureAwait(false);
// Send the request and wait for answer
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
if (!subResult)
{
await socket.Close(handler).ConfigureAwait(false);
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(null, subResult.Error);
}
}
else
{
handler.Confirmed = true;
// No request to be sent, so just mark the subscription as comfirmed
subscription.Confirmed = true;
}
socket.ShouldReconnect = true;
return new CallResult<UpdateSubscription>(new UpdateSubscription(socket, handler), null);
socketConnection.ShouldReconnect = true;
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription), null);
}
/// <summary>
/// Sends the subscribe request and waits for a response to that request
/// </summary>
/// <param name="socket">The connection to send the request on</param>
/// <param name="request">The request to send</param>
/// <param name="socketConnection">The connection to send the request on</param>
/// <param name="request">The request to send, will be serialized to json</param>
/// <param name="subscription">The subscription the request is for</param>
/// <returns></returns>
protected internal virtual async Task<CallResult<bool>> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription)
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{
CallResult<object>? callResult = null;
await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false);
await socketConnection.SendAndWaitAsync(request, ResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true)
subscription.Confirmed = true;
@ -216,33 +221,33 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Query for data
/// 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="request">The request to send</param>
/// <param name="authenticated">Whether the socket should be authenticated</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>> Query<T>(object request, bool authenticated)
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
{
return Query<T>(BaseAddress, request, authenticated);
return QueryAsync<T>(BaseAddress, request, authenticated);
}
/// <summary>
/// Query for data
/// Send a query on a socket connection and wait for the response
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <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>> Query<T>(string url, object request, bool authenticated)
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
{
SocketConnection socket;
SocketConnection socketConnection;
var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
socket = GetWebsocket(url, authenticated);
socketConnection = GetSocketConnection(url, authenticated);
if (SocketCombineTarget == 1)
{
// Can release early when only a single sub per connection
@ -250,7 +255,7 @@ namespace CryptoExchange.Net
released = true;
}
var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(default, connectResult.Error);
}
@ -262,13 +267,13 @@ namespace CryptoExchange.Net
semaphoreSlim.Release();
}
if (socket.PausedActivity)
if (socketConnection.PausedActivity)
{
log.Write(LogVerbosity.Info, "Socket has been paused, can't send query at this moment");
log.Write(LogLevel.Information, "Socket has been paused, can't send query at this moment");
return new CallResult<T>(default, new ServerError("Socket is paused"));
}
return await QueryAndWait<T>(socket, request).ConfigureAwait(false);
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
}
/// <summary>
@ -278,10 +283,10 @@ namespace CryptoExchange.Net
/// <param name="socket">The connection to send and wait on</param>
/// <param name="request">The request to send</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAndWait<T>(SocketConnection socket, object request)
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.SendAndWait(request, ResponseTimeout, data =>
await socket.SendAndWaitAsync(request, ResponseTimeout, data =>
{
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false;
@ -294,27 +299,27 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Checks if a socket needs to be connected and does so if needed
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
/// </summary>
/// <param name="socket">The connection to check</param>
/// <param name="authenticated">Whether the socket should authenticated</param>
/// <returns></returns>
protected virtual async Task<CallResult<bool>> ConnectIfNeeded(SocketConnection socket, bool authenticated)
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
{
if (socket.Connected)
return new CallResult<bool>(true, null);
var connectResult = await ConnectSocket(socket).ConfigureAwait(false);
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
if (!connectResult)
return new CallResult<bool>(false, connectResult.Error);
if (!authenticated || socket.Authenticated)
return new CallResult<bool>(true, null);
var result = await AuthenticateSocket(socket).ConfigureAwait(false);
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
{
log.Write(LogVerbosity.Warning, "Socket authentication failed");
log.Write(LogLevel.Warning, "Socket authentication failed");
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(false, result.Error);
}
@ -322,54 +327,64 @@ namespace CryptoExchange.Net
socket.Authenticated = true;
return new CallResult<bool>(true, null);
}
/// <summary>
/// Needs to check if a received message was an answer to a query request (preferable by id) and set the callResult out to whatever the response is
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the query that was send (the request parameter).
/// For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
/// anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
/// if not some other method has be implemented to match the messages).
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
/// </summary>
/// <typeparam name="T">The type of response</typeparam>
/// <param name="s">The socket connection</param>
/// <typeparam name="T">The type of response that is expected on the query</typeparam>
/// <param name="socketConnection">The socket connection</param>
/// <param name="request">The request that a response is awaited for</param>
/// <param name="data">The message</param>
/// <param name="data">The message received from the server</param>
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
/// <returns>True if the message was a response to the query</returns>
protected internal abstract bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, [NotNullWhen(true)]out CallResult<T>? callResult);
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)]out CallResult<T>? callResult);
/// <summary>
/// Needs to check if a received message was an answer to a subscription request (preferable by id) and set the callResult out to whatever the response is
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter).
/// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
/// anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
/// if not some other method has be implemented to match the messages).
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
/// </summary>
/// <param name="s">The socket connection</param>
/// <param name="subscription"></param>
/// <param name="request">The request that a response is awaited for</param>
/// <param name="message">The message</param>
/// <param name="socketConnection">The socket connection</param>
/// <param name="subscription">A subscription that waiting for a subscription response</param>
/// <param name="request">The request that the subscription sent</param>
/// <param name="data">The message received from the server</param>
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
/// <returns>True if the message was a response to the subscription request</returns>
protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult<object>? callResult);
protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
/// <summary>
/// Needs to check if a received message matches a handler. Typically if an update message matches the request
/// 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="message">The received data</param>
/// <param name="request">The subscription request</param>
/// <returns></returns>
/// <returns>True if the message is for the subscription which sent the request</returns>
protected internal abstract bool MessageMatchesHandler(JToken message, object request);
/// <summary>
/// Needs to check if a received message matches a handler. Typically if an received message matches a ping request or a other information pushed from the the server
/// 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="message">The received data</param>
/// <param name="identifier">The string identifier of the handler</param>
/// <returns></returns>
/// <returns>True if the message is for the handler which has the identifier</returns>
protected internal abstract bool MessageMatchesHandler(JToken message, string identifier);
/// <summary>
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
/// </summary>
/// <param name="s"></param>
/// <param name="socketConnection">The socket connection that should be authenticated</param>
/// <returns></returns>
protected internal abstract Task<CallResult<bool>> AuthenticateSocket(SocketConnection s);
protected internal abstract Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socketConnection);
/// <summary>
/// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway
/// </summary>
/// <param name="connection">The connection on which to unsubscribe</param>
/// <param name="s">The subscription to unsubscribe</param>
/// <param name="subscriptionToUnsub">The subscription to unsubscribe</param>
/// <returns></returns>
protected internal abstract Task<bool> Unsubscribe(SocketConnection connection, SocketSubscription s);
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
/// <summary>
/// Optional handler to interpolate data before sending it to the handlers
@ -382,7 +397,7 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Add a handler for a subscription
/// Add a subscription to a connection
/// </summary>
/// <typeparam name="T">The type of data the subscription expects</typeparam>
/// <param name="request">The request of the subscription</param>
@ -391,31 +406,32 @@ namespace CryptoExchange.Net
/// <param name="connection">The socket connection the handler is on</param>
/// <param name="dataHandler">The handler of the data received</param>
/// <returns></returns>
protected virtual SocketSubscription AddHandler<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<T> dataHandler)
protected virtual SocketSubscription AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler)
{
void InternalHandler(SocketConnection socketWrapper, JToken data)
void InternalHandler(MessageEvent messageEvent)
{
if (typeof(T) == typeof(string))
{
dataHandler((T) Convert.ChangeType(data.ToString(), typeof(T)));
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
dataHandler(new DataEvent<T>(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
return;
}
var desResult = Deserialize<T>(data, false);
var desResult = Deserialize<T>(messageEvent.JsonData, false);
if (!desResult)
{
log.Write(LogVerbosity.Warning, $"Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
log.Write(LogLevel.Warning, $"Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
return;
}
dataHandler(desResult.Data);
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
}
var handler = request == null
var subscription = request == null
? SocketSubscription.CreateForIdentifier(identifier!, userSubscription, InternalHandler)
: SocketSubscription.CreateForRequest(request, userSubscription, InternalHandler);
connection.AddHandler(handler);
return handler;
connection.AddSubscription(subscription);
return subscription;
}
/// <summary>
@ -423,12 +439,12 @@ namespace CryptoExchange.Net
/// </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>
protected void AddGenericHandler(string identifier, Action<SocketConnection, JToken> action)
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
{
genericHandlers.Add(identifier, action);
var handler = SocketSubscription.CreateForIdentifier(identifier, false, action);
var subscription = SocketSubscription.CreateForIdentifier(identifier, false, action);
foreach (var connection in sockets.Values)
connection.AddHandler(handler);
connection.AddSubscription(subscription);
}
/// <summary>
@ -437,14 +453,14 @@ namespace CryptoExchange.Net
/// <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 GetWebsocket(string address, bool authenticated)
protected virtual SocketConnection GetSocketConnection(string address, bool authenticated)
{
var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/')
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.HandlerCount).FirstOrDefault();
&& (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.HandlerCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.HandlerCount >= SocketCombineTarget)))
if (result.SubscriptionCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= SocketCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return result;
@ -453,15 +469,15 @@ namespace CryptoExchange.Net
// Create new socket
var socket = CreateSocket(address);
var socketWrapper = new SocketConnection(this, socket);
socketWrapper.UnhandledMessage += HandleUnhandledMessage;
var socketConnection = new SocketConnection(this, socket);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
foreach (var kvp in genericHandlers)
{
var handler = SocketSubscription.CreateForIdentifier(kvp.Key, false, kvp.Value);
socketWrapper.AddHandler(handler);
socketConnection.AddSubscription(handler);
}
return socketWrapper;
return socketConnection;
}
/// <summary>
@ -477,9 +493,9 @@ namespace CryptoExchange.Net
/// </summary>
/// <param name="socketConnection">The socket to connect</param>
/// <returns></returns>
protected virtual async Task<CallResult<bool>> ConnectSocket(SocketConnection socketConnection)
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
{
if (await socketConnection.Socket.Connect().ConfigureAwait(false))
if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false))
{
sockets.TryAdd(socketConnection.Socket.Id, socketConnection);
return new CallResult<bool>(true, null);
@ -497,23 +513,23 @@ namespace CryptoExchange.Net
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(log, address);
log.Write(LogVerbosity.Debug, "Created new socket for " + address);
log.Write(LogLevel.Debug, "Created new socket for " + address);
if (apiProxy != null)
socket.SetProxy(apiProxy.Host, apiProxy.Port);
socket.SetProxy(apiProxy);
socket.Timeout = SocketNoDataTimeout;
socket.DataInterpreterBytes = dataInterpreterBytes;
socket.DataInterpreterString = dataInterpreterString;
socket.OnError += e =>
{
log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e);
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: " + e);
};
return socket;
}
/// <summary>
/// Periodically sends an object to a socket
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
@ -532,7 +548,7 @@ namespace CryptoExchange.Net
break;
if (sockets.Any())
log.Write(LogVerbosity.Debug, "Sending periodic");
log.Write(LogLevel.Debug, "Sending periodic");
foreach (var socket in sockets.Values)
{
@ -549,7 +565,7 @@ namespace CryptoExchange.Net
}
catch (Exception ex)
{
log.Write(LogVerbosity.Warning, "Periodic send failed: " + ex);
log.Write(LogLevel.Warning, "Periodic send failed: " + ex);
}
}
}
@ -558,26 +574,26 @@ namespace CryptoExchange.Net
/// <summary>
/// Unsubscribe from a stream
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task Unsubscribe(UpdateSubscription subscription)
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
log.Write(LogVerbosity.Info, "Closing subscription");
await subscription.Close().ConfigureAwait(false);
log.Write(LogLevel.Information, "Closing subscription");
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
public virtual async Task UnsubscribeAll()
public virtual async Task UnsubscribeAllAsync()
{
log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.HandlerCount)} subscriptions");
log.Write(LogLevel.Debug, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions");
await Task.Run(async () =>
{
@ -585,7 +601,7 @@ namespace CryptoExchange.Net
{
var socketList = sockets.Values;
foreach (var sub in socketList)
tasks.Add(sub.Close());
tasks.Add(sub.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@ -600,8 +616,8 @@ namespace CryptoExchange.Net
disposing = true;
periodicEvent?.Set();
periodicEvent?.Dispose();
log.Write(LogVerbosity.Debug, "Disposing socket client, closing all subscriptions");
Task.Run(UnsubscribeAll).Wait();
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult();
semaphoreSlim?.Dispose();
base.Dispose();
}

View File

@ -1,422 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using SuperSocket.ClientEngine;
using SuperSocket.ClientEngine.Proxy;
using WebSocket4Net;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Socket implementation
/// </summary>
public class BaseSocket: IWebsocket
{
internal static int lastStreamId;
private static readonly object streamIdLock = new object();
/// <summary>
/// Socket
/// </summary>
protected WebSocket? socket;
/// <summary>
/// Log
/// </summary>
protected Log log;
private readonly object socketLock = new object();
/// <summary>
/// Error handlers
/// </summary>
protected readonly List<Action<Exception>> errorHandlers = new List<Action<Exception>>();
/// <summary>
/// Open handlers
/// </summary>
protected readonly List<Action> openHandlers = new List<Action>();
/// <summary>
/// Close handlers
/// </summary>
protected readonly List<Action> closeHandlers = new List<Action>();
/// <summary>
/// Message handlers
/// </summary>
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
private readonly IDictionary<string, string> cookies;
private readonly IDictionary<string, string> headers;
private HttpConnectProxy? proxy;
/// <summary>
/// Id
/// </summary>
public int Id { get; }
/// <summary>
/// If is reconnecting
/// </summary>
public bool Reconnecting { get; set; }
/// <summary>
/// Origin
/// </summary>
public string? Origin { get; set; }
/// <summary>
/// Url
/// </summary>
public string Url { get; }
/// <summary>
/// Is closed
/// </summary>
public bool IsClosed => socket?.State == null || socket.State == WebSocketState.Closed;
/// <summary>
/// Is open
/// </summary>
public bool IsOpen => socket?.State == WebSocketState.Open;
/// <summary>
/// Protocols
/// </summary>
public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
/// <summary>
/// Interpreter for bytes
/// </summary>
public Func<byte[], string>? DataInterpreterBytes { get; set; }
/// <summary>
/// Interpreter for strings
/// </summary>
public Func<string, string>? DataInterpreterString { get; set; }
/// <summary>
/// Last action time
/// </summary>
public DateTime LastActionTime { get; private set; }
/// <summary>
/// Timeout
/// </summary>
public TimeSpan Timeout { get; set; }
private Task? timeoutTask;
/// <summary>
/// Socket state
/// </summary>
public WebSocketState SocketState => socket?.State ?? WebSocketState.None;
/// <summary>
/// ctor
/// </summary>
/// <param name="log"></param>
/// <param name="url"></param>
public BaseSocket(Log log, string url):this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>())
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="log"></param>
/// <param name="url"></param>
/// <param name="cookies"></param>
/// <param name="headers"></param>
public BaseSocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
{
Id = NextStreamId();
this.log = log;
Url = url;
this.cookies = cookies;
this.headers = headers;
}
private void HandleByteData(byte[] data)
{
if (DataInterpreterBytes == null)
throw new Exception("Byte interpreter not set while receiving byte data");
try
{
var message = DataInterpreterBytes(data);
Handle(messageHandlers, message);
}
catch (Exception ex)
{
log.Write(LogVerbosity.Error, $"{Id} Something went wrong while processing a byte message from the socket: {ex}");
}
}
private void HandleStringData(string data)
{
try
{
if (DataInterpreterString != null)
data = DataInterpreterString(data);
Handle(messageHandlers, data);
}
catch (Exception ex)
{
log.Write(LogVerbosity.Error, $"{Id} Something went wrong while processing a string message from the socket: {ex}");
}
}
/// <summary>
/// On close
/// </summary>
public event Action OnClose
{
add => closeHandlers.Add(value);
remove => closeHandlers.Remove(value);
}
/// <summary>
/// On message
/// </summary>
public event Action<string> OnMessage
{
add => messageHandlers.Add(value);
remove => messageHandlers.Remove(value);
}
/// <summary>
/// On error
/// </summary>
public event Action<Exception> OnError
{
add => errorHandlers.Add(value);
remove => errorHandlers.Remove(value);
}
/// <summary>
/// On open
/// </summary>
public event Action OnOpen
{
add => openHandlers.Add(value);
remove => openHandlers.Remove(value);
}
/// <summary>
/// Handle
/// </summary>
/// <param name="handlers"></param>
protected void Handle(List<Action> handlers)
{
LastActionTime = DateTime.UtcNow;
foreach (var handle in new List<Action>(handlers))
handle?.Invoke();
}
/// <summary>
/// Handle
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="handlers"></param>
/// <param name="data"></param>
protected void Handle<T>(List<Action<T>> handlers, T data)
{
LastActionTime = DateTime.UtcNow;
foreach (var handle in new List<Action<T>>(handlers))
handle?.Invoke(data);
}
/// <summary>
/// Checks if timed out
/// </summary>
/// <returns></returns>
protected async Task CheckTimeout()
{
while (true)
{
lock (socketLock)
{
if (socket == null || socket.State != WebSocketState.Open)
return;
if (DateTime.UtcNow - LastActionTime > Timeout)
{
log.Write(LogVerbosity.Warning, $"No data received for {Timeout}, reconnecting socket");
_ = Close().ConfigureAwait(false);
return;
}
}
await Task.Delay(500).ConfigureAwait(false);
}
}
/// <summary>
/// Close socket
/// </summary>
/// <returns></returns>
public virtual async Task Close()
{
await Task.Run(() =>
{
lock (socketLock)
{
if (socket == null || IsClosed)
{
log?.Write(LogVerbosity.Debug, $"Socket {Id} was already closed/disposed");
return;
}
var waitLock = new object();
log?.Write(LogVerbosity.Debug, $"Socket {Id} closing");
ManualResetEvent? evnt = new ManualResetEvent(false);
var handler = new EventHandler((o, a) =>
{
lock(waitLock)
evnt?.Set();
});
socket.Closed += handler;
socket.Close();
evnt.WaitOne(2000);
lock (waitLock)
{
socket.Closed -= handler;
evnt.Dispose();
evnt = null;
}
log?.Write(LogVerbosity.Debug, $"Socket {Id} closed");
}
}).ConfigureAwait(false);
}
/// <summary>
/// Reset socket
/// </summary>
public virtual void Reset()
{
lock (socketLock)
{
log.Write(LogVerbosity.Debug, $"Socket {Id} resetting");
socket?.Dispose();
socket = null;
}
}
/// <summary>
/// Send data
/// </summary>
/// <param name="data"></param>
public virtual void Send(string data)
{
socket?.Send(data);
}
/// <summary>
/// Connect socket
/// </summary>
/// <returns></returns>
public virtual Task<bool> Connect()
{
if (socket == null)
{
socket = new WebSocket(Url, cookies: cookies.ToList(), customHeaderItems: headers.ToList(), origin: Origin ?? "")
{
EnableAutoSendPing = true,
AutoSendPingInterval = 10
};
if (proxy != null)
socket.Proxy = proxy;
socket.Security.EnabledSslProtocols = SSLProtocols;
socket.Opened += (o, s) => Handle(openHandlers);
socket.Closed += (o, s) => Handle(closeHandlers);
socket.Error += (o, s) => Handle(errorHandlers, s.Exception);
socket.MessageReceived += (o, s) => HandleStringData(s.Message);
socket.DataReceived += (o, s) => HandleByteData(s.Data);
}
return Task.Run(() =>
{
bool connected;
lock (socketLock)
{
log?.Write(LogVerbosity.Debug, $"Socket {Id} connecting");
var waitLock = new object();
ManualResetEvent? evnt = new ManualResetEvent(false);
var handler = new EventHandler((o, a) =>
{
lock (waitLock)
evnt?.Set();
});
var errorHandler = new EventHandler<ErrorEventArgs>((o, a) =>
{
lock(waitLock)
evnt?.Set();
});
socket.Opened += handler;
socket.Closed += handler;
socket.Error += errorHandler;
socket.Open();
evnt.WaitOne(TimeSpan.FromSeconds(15));
lock (waitLock)
{
socket.Opened -= handler;
socket.Closed -= handler;
socket.Error -= errorHandler;
evnt.Dispose();
evnt = null;
}
connected = socket.State == WebSocketState.Open;
if (connected)
{
log?.Write(LogVerbosity.Debug, $"Socket {Id} connected");
if ((timeoutTask == null || timeoutTask.IsCompleted) && Timeout != default)
timeoutTask = Task.Run(CheckTimeout);
}
else
{
log?.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State);
}
}
if (socket.State == WebSocketState.Connecting)
socket.Close();
return connected;
});
}
/// <summary>
/// Set a proxy
/// </summary>
/// <param name="host"></param>
/// <param name="port"></param>
public virtual void SetProxy(string host, int port)
{
proxy = IPAddress.TryParse(host, out var address)
? new HttpConnectProxy(new IPEndPoint(address, port))
: new HttpConnectProxy(new DnsEndPoint(host, port));
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
lock (socketLock)
{
if (socket != null)
log?.Write(LogVerbosity.Debug, $"Socket {Id} disposing websocket");
socket?.Dispose();
socket = null;
errorHandlers.Clear();
openHandlers.Clear();
closeHandlers.Clear();
messageHandlers.Clear();
}
}
private static int NextStreamId()
{
lock (streamIdLock)
{
lastStreamId++;
return lastStreamId;
}
}
}
}

View File

@ -0,0 +1,561 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.WebSockets;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// A wrapper around the ClientWebSocket
/// </summary>
public class CryptoExchangeWebSocketClient : IWebsocket
{
internal static int lastStreamId;
private static readonly object streamIdLock = new object();
private ClientWebSocket _socket;
private Task? _sendTask;
private Task? _receiveTask;
private Task? _timeoutTask;
private readonly AutoResetEvent _sendEvent;
private readonly ConcurrentQueue<byte[]> _sendBuffer;
private readonly IDictionary<string, string> cookies;
private readonly IDictionary<string, string> headers;
private CancellationTokenSource _ctsSource;
private bool _closing;
/// <summary>
/// Log
/// </summary>
protected Log log;
/// <summary>
/// Handlers for when an error happens on the socket
/// </summary>
protected readonly List<Action<Exception>> errorHandlers = new List<Action<Exception>>();
/// <summary>
/// Handlers for when the socket connection is opened
/// </summary>
protected readonly List<Action> openHandlers = new List<Action>();
/// <summary>
/// Handlers for when the connection is closed
/// </summary>
protected readonly List<Action> closeHandlers = new List<Action>();
/// <summary>
/// Handlers for when a message is received
/// </summary>
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
/// <summary>
/// The id of this socket
/// </summary>
public int Id { get; }
/// <inheritdoc />
public string? Origin { get; set; }
/// <summary>
/// Whether this socket is currently reconnecting
/// </summary>
public bool Reconnecting { get; set; }
/// <summary>
/// The timestamp this socket has been active for the last time
/// </summary>
public DateTime LastActionTime { get; private 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>
/// Url this socket connects to
/// </summary>
public string Url { get; }
/// <summary>
/// If the connection is closed
/// </summary>
public bool IsClosed => _socket.State == WebSocketState.Closed;
/// <summary>
/// If the connection is open
/// </summary>
public bool IsOpen => _socket.State == WebSocketState.Open;
/// <summary>
/// Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION
/// </summary>
public SslProtocols SSLProtocols { get; set; }
private Encoding _encoding = Encoding.UTF8;
/// <summary>
/// Encoding used for decoding the received bytes into a string
/// </summary>
public Encoding? Encoding
{
get => _encoding;
set
{
if(value != null)
_encoding = value;
}
}
/// <summary>
/// The timespan no data is received on the socket. If no data is received within this time an error is generated
/// </summary>
public TimeSpan Timeout { get; set; }
/// <summary>
/// Socket closed event
/// </summary>
public event Action OnClose
{
add => closeHandlers.Add(value);
remove => closeHandlers.Remove(value);
}
/// <summary>
/// Socket message received event
/// </summary>
public event Action<string> OnMessage
{
add => messageHandlers.Add(value);
remove => messageHandlers.Remove(value);
}
/// <summary>
/// Socket error event
/// </summary>
public event Action<Exception> OnError
{
add => errorHandlers.Add(value);
remove => errorHandlers.Remove(value);
}
/// <summary>
/// Socket opened event
/// </summary>
public event Action OnOpen
{
add => openHandlers.Add(value);
remove => openHandlers.Remove(value);
}
/// <summary>
/// ctor
/// </summary>
/// <param name="log">The log object to use</param>
/// <param name="url">The url the socket should connect to</param>
public CryptoExchangeWebSocketClient(Log log, string url) : this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>())
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="log">The log object to use</param>
/// <param name="url">The url the socket should connect to</param>
/// <param name="cookies">Cookies to sent in the socket connection request</param>
/// <param name="headers">Headers to sent in the socket connection request</param>
public CryptoExchangeWebSocketClient(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
{
Id = NextStreamId();
this.log = log;
Url = url;
this.cookies = cookies;
this.headers = headers;
_sendEvent = new AutoResetEvent(false);
_sendBuffer = new ConcurrentQueue<byte[]>();
_ctsSource = new CancellationTokenSource();
_socket = CreateSocket();
}
/// <summary>
/// Set a proxy to use. Should be set before connecting
/// </summary>
/// <param name="proxy"></param>
public virtual void SetProxy(ApiProxy proxy)
{
_socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port);
if (proxy.Login != null)
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
}
/// <summary>
/// Connect the websocket
/// </summary>
/// <returns>True if successfull</returns>
public virtual async Task<bool> ConnectAsync()
{
log.Write(LogLevel.Debug, $"Socket {Id} connecting");
try
{
using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await _socket.ConnectAsync(new Uri(Url), default).ConfigureAwait(false);
Handle(openHandlers);
}
catch (Exception e)
{
log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
return false;
}
log.Write(LogLevel.Debug, $"Socket {Id} connected");
_sendTask = Task.Run(async () => await SendLoopAsync().ConfigureAwait(false));
_receiveTask = Task.Run(ReceiveLoopAsync);
if (Timeout != default)
_timeoutTask = Task.Run(CheckTimeoutAsync);
return true;
}
/// <summary>
/// Send data over the websocket
/// </summary>
/// <param name="data">Data to send</param>
public virtual void Send(string data)
{
if (_closing)
throw new InvalidOperationException("Can't send data when socket is not connected");
var bytes = _encoding.GetBytes(data);
_sendBuffer.Enqueue(bytes);
_sendEvent.Set();
}
/// <summary>
/// Close the websocket
/// </summary>
/// <returns></returns>
public virtual async Task CloseAsync()
{
log.Write(LogLevel.Debug, $"Socket {Id} closing");
await CloseInternalAsync(true, true).ConfigureAwait(false);
}
/// <summary>
/// Internal close method, will wait for each task to complete to gracefully close
/// </summary>
/// <param name="waitSend"></param>
/// <param name="waitReceive"></param>
/// <returns></returns>
private async Task CloseInternalAsync(bool waitSend, bool waitReceive)
{
if (_closing)
return;
_closing = true;
var tasksToAwait = new List<Task>();
if (_socket.State == WebSocketState.Open)
tasksToAwait.Add(_socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default));
_ctsSource.Cancel();
_sendEvent.Set();
if (waitSend)
tasksToAwait.Add(_sendTask!);
if (waitReceive)
tasksToAwait.Add(_receiveTask!);
if (_timeoutTask != null)
tasksToAwait.Add(_timeoutTask);
await Task.WhenAll(tasksToAwait).ConfigureAwait(false);
log.Write(LogLevel.Debug, $"Socket {Id} closed");
Handle(closeHandlers);
}
/// <summary>
/// Dispose the socket
/// </summary>
public void Dispose()
{
log.Write(LogLevel.Debug, $"Socket {Id} disposing");
_socket.Dispose();
_ctsSource.Dispose();
errorHandlers.Clear();
openHandlers.Clear();
closeHandlers.Clear();
messageHandlers.Clear();
}
/// <summary>
/// Reset the socket so a new connection can be attempted after it has been connected before
/// </summary>
public void Reset()
{
log.Write(LogLevel.Debug, $"Socket {Id} resetting");
_ctsSource = new CancellationTokenSource();
_closing = false;
_socket = CreateSocket();
}
/// <summary>
/// Create the socket object
/// </summary>
private ClientWebSocket CreateSocket()
{
var cookieContainer = new CookieContainer();
foreach (var cookie in cookies)
cookieContainer.Add(new Cookie(cookie.Key, cookie.Value));
var socket = new ClientWebSocket();
socket.Options.Cookies = cookieContainer;
foreach (var header in headers)
socket.Options.SetRequestHeader(header.Key, header.Value);
socket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
return socket;
}
/// <summary>
/// Loop for sending data
/// </summary>
/// <returns></returns>
private async Task SendLoopAsync()
{
while (true)
{
_sendEvent.WaitOne();
if (_closing)
break;
if (!_sendBuffer.TryDequeue(out var data))
continue;
try
{
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// cancelled
break;
}
catch (WebSocketException wse)
{
// Connection closed unexpectedly
Handle(errorHandlers, wse);
await CloseInternalAsync(false, true).ConfigureAwait(false);
break;
}
}
}
/// <summary>
/// Loop for receiving and reassembling data
/// </summary>
/// <returns></returns>
private async Task ReceiveLoopAsync()
{
var buffer = new ArraySegment<byte>(new byte[4096]);
var received = 0;
while (true)
{
if (_closing)
break;
MemoryStream? memoryStream = null;
WebSocketReceiveResult? receiveResult = null;
bool multiPartMessage = false;
while (true)
{
try
{
receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false);
received += receiveResult.Count;
}
catch (TaskCanceledException)
{
// Cancelled
break;
}
catch (WebSocketException wse)
{
// Connection closed unexpectedly
Handle(errorHandlers, wse);
await CloseInternalAsync(true, false).ConfigureAwait(false);
break;
}
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
// Connection closed unexpectedly
log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
await CloseInternalAsync(true, false).ConfigureAwait(false);
break;
}
if (!receiveResult.EndOfMessage)
{
// We received data, but it is not complete, write it to a memory stream for reassembling
multiPartMessage = true;
if (memoryStream == null)
memoryStream = new MemoryStream();
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
else
{
if (!multiPartMessage)
// Received a complete message and it's not multi part
HandleMessage(buffer.Array, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
else
// Received the end of a multipart message, write to memory stream for reassembling
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
break;
}
}
if (receiveResult?.MessageType == WebSocketMessageType.Close)
{
// Received close message
break;
}
if (receiveResult == null || _closing)
{
// Error during receiving or cancellation requested, stop.
break;
}
if (multiPartMessage)
{
// Reassemble complete message from memory stream
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
memoryStream.Dispose();
}
}
}
/// <summary>
/// Handles the message
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <param name="messageType"></param>
private void HandleMessage(byte[] data, int offset, int count, WebSocketMessageType messageType)
{
string strData;
if (messageType == WebSocketMessageType.Binary)
{
if (DataInterpreterBytes == null)
throw new Exception("Byte interpreter not set while receiving byte data");
try
{
strData = DataInterpreterBytes(data);
}
catch(Exception e)
{
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
return;
}
}
else
strData = _encoding.GetString(data, offset, count);
if (DataInterpreterString != null)
{
try
{
strData = DataInterpreterString(strData);
}
catch(Exception e)
{
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
return;
}
}
try
{
Handle(messageHandlers, strData);
}
catch(Exception e)
{
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
return;
}
}
/// <summary>
/// Checks if there is no data received for a period longer than the specified timeout
/// </summary>
/// <returns></returns>
protected async Task CheckTimeoutAsync()
{
log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Timeout}");
while (true)
{
if (_closing)
return;
if (DateTime.UtcNow - LastActionTime > Timeout)
{
log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Timeout}, reconnecting socket");
_ = CloseAsync().ConfigureAwait(false);
return;
}
try
{
await Task.Delay(500, _ctsSource.Token).ConfigureAwait(false);
}
catch(TaskCanceledException)
{
// cancelled
return;
}
}
}
/// <summary>
/// Helper to invoke handlers
/// </summary>
/// <param name="handlers"></param>
protected void Handle(List<Action> handlers)
{
LastActionTime = DateTime.UtcNow;
foreach (var handle in new List<Action>(handlers))
handle?.Invoke();
}
/// <summary>
/// Helper to invoke handlers
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="handlers"></param>
/// <param name="data"></param>
protected void Handle<T>(List<Action<T>> handlers, T data)
{
LastActionTime = DateTime.UtcNow;
foreach (var handle in new List<Action<T>>(handlers))
handle?.Invoke(data);
}
/// <summary>
/// Get the next identifier
/// </summary>
/// <returns></returns>
private static int NextStreamId()
{
lock (streamIdLock)
{
lastStreamId++;
return lastStreamId;
}
}
}
}

View File

@ -0,0 +1,72 @@
using System;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// An update received from a socket update subscription
/// </summary>
/// <typeparam name="T">The type of the data</typeparam>
public class DataEvent<T>
{
/// <summary>
/// The timestamp the data was received
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// The topic of the update, what symbol/asset etc..
/// </summary>
public string? Topic { get; set; }
/// <summary>
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
/// </summary>
public string? OriginalData { get; set; }
/// <summary>
/// The received data deserialized into an object
/// </summary>
public T Data { get; set; }
internal DataEvent(T data, DateTime timestamp)
{
Data = data;
Timestamp = timestamp;
}
internal DataEvent(T data, string? topic, DateTime timestamp)
{
Data = data;
Topic = topic;
Timestamp = timestamp;
}
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp)
{
Data = data;
Topic = topic;
OriginalData = originalData;
Timestamp = timestamp;
}
/// <summary>
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and Timestamp will be copied over
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <returns></returns>
public DataEvent<K> As<K>(K data)
{
return new DataEvent<K>(data, Topic, OriginalData, Timestamp);
}
/// <summary>
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <param name="topic">The new topic</param>
/// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic)
{
return new DataEvent<K>(data, topic, OriginalData, Timestamp);
}
}
}

View File

@ -0,0 +1,43 @@
using Newtonsoft.Json.Linq;
using System;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Message received event
/// </summary>
public class MessageEvent
{
/// <summary>
/// The connection the message was received on
/// </summary>
public SocketConnection Connection { get; set; }
/// <summary>
/// The json object of the data
/// </summary>
public JToken JsonData { get; set; }
/// <summary>
/// The originally received string data
/// </summary>
public string? OriginalData { get; set; }
/// <summary>
/// The timestamp of when the data was received
/// </summary>
public DateTime ReceivedTimestamp { get; set; }
/// <summary>
///
/// </summary>
/// <param name="connection"></param>
/// <param name="jsonData"></param>
/// <param name="originalData"></param>
/// <param name="timestamp"></param>
public MessageEvent(SocketConnection connection, JToken jsonData, string? originalData, DateTime timestamp)
{
Connection = connection;
JsonData = jsonData;
OriginalData = originalData;
ReceivedTimestamp = timestamp;
}
}
}

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Sockets
{
@ -42,12 +43,12 @@ namespace CryptoExchange.Net.Sockets
public event Action<JToken>? UnhandledMessage;
/// <summary>
/// The amount of handlers
/// The amount of subscriptions on this connection
/// </summary>
public int HandlerCount
public int SubscriptionCount
{
get { lock (handlersLock)
return handlers.Count(h => h.UserSubscription); }
get { lock (subscriptionLock)
return subscriptions.Count(h => h.UserSubscription); }
}
/// <summary>
@ -60,11 +61,11 @@ namespace CryptoExchange.Net.Sockets
public bool Connected { get; private set; }
/// <summary>
/// The socket
/// The underlying socket
/// </summary>
public IWebsocket Socket { get; set; }
/// <summary>
/// If should reconnect upon closing
/// If the socket should be reconnected upon closing
/// </summary>
public bool ShouldReconnect { get; set; }
@ -84,7 +85,7 @@ namespace CryptoExchange.Net.Sockets
if (pausedActivity != value)
{
pausedActivity = value;
log.Write(LogVerbosity.Debug, "Paused activity: " + value);
log.Write(LogLevel.Debug, "Paused activity: " + value);
if(pausedActivity) ActivityPaused?.Invoke();
else ActivityUnpaused?.Invoke();
}
@ -92,8 +93,8 @@ namespace CryptoExchange.Net.Sockets
}
private bool pausedActivity;
private readonly List<SocketSubscription> handlers;
private readonly object handlersLock = new object();
private readonly List<SocketSubscription> subscriptions;
private readonly object subscriptionLock = new object();
private bool lostTriggered;
private readonly Log log;
@ -113,7 +114,7 @@ namespace CryptoExchange.Net.Sockets
pendingRequests = new List<PendingRequest>();
handlers = new List<SocketSubscription>();
subscriptions = new List<SocketSubscription>();
Socket = socket;
Socket.Timeout = client.SocketNoDataTimeout;
@ -137,9 +138,14 @@ namespace CryptoExchange.Net.Sockets
};
}
/// <summary>
/// Process a message received by the socket
/// </summary>
/// <param name="data"></param>
private void ProcessMessage(string data)
{
log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data);
var timestamp = DateTime.UtcNow;
log.Write(LogLevel.Trace, $"Socket {Socket.Id} received data: " + data);
if (string.IsNullOrEmpty(data)) return;
var tokenData = data.ToJToken(log);
@ -151,12 +157,14 @@ namespace CryptoExchange.Net.Sockets
return;
}
var messageEvent = new MessageEvent(this, tokenData, socketClient.OutputOriginalData ? data: null, timestamp);
var handledResponse = false;
PendingRequest[] requests;
lock(pendingRequests)
{
requests = pendingRequests.ToArray();
}
// Check if this message is an answer on any pending requests
foreach (var pendingRequest in requests)
{
if (pendingRequest.Check(tokenData))
@ -176,51 +184,54 @@ namespace CryptoExchange.Net.Sockets
}
}
if (!HandleData(tokenData) && !handledResponse)
// Message was not a request response, check data handlers
if (!HandleData(messageEvent) && !handledResponse)
{
if (!socketClient.UnhandledMessageExpected)
log.Write(LogVerbosity.Warning, "Message not handled: " + tokenData);
log.Write(LogLevel.Warning, $"Socket {Socket.Id} Message not handled: " + tokenData);
UnhandledMessage?.Invoke(tokenData);
}
}
/// <summary>
/// Add handler
/// Add subscription to this connection
/// </summary>
/// <param name="handler"></param>
public void AddHandler(SocketSubscription handler)
/// <param name="subscription"></param>
public void AddSubscription(SocketSubscription subscription)
{
lock(handlersLock)
handlers.Add(handler);
lock(subscriptionLock)
subscriptions.Add(subscription);
}
private bool HandleData(JToken tokenData)
private bool HandleData(MessageEvent messageEvent)
{
SocketSubscription? currentSubscription = null;
try
{
var handled = false;
var sw = Stopwatch.StartNew();
lock (handlersLock)
// Loop the subscriptions to check if any of them signal us that the message is for them
lock (subscriptionLock)
{
foreach (var handler in handlers.ToList())
foreach (var subscription in subscriptions.ToList())
{
currentSubscription = handler;
if (handler.Request == null)
currentSubscription = subscription;
if (subscription.Request == null)
{
if (socketClient.MessageMatchesHandler(tokenData, handler.Identifier!))
if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Identifier!))
{
handled = true;
handler.MessageHandler(this, tokenData);
subscription.MessageHandler(messageEvent);
}
}
else
{
if (socketClient.MessageMatchesHandler(tokenData, handler.Request))
if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Request))
{
handled = true;
tokenData = socketClient.ProcessTokenData(tokenData);
handler.MessageHandler(this, tokenData);
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData);
subscription.MessageHandler(messageEvent);
}
}
}
@ -228,29 +239,29 @@ namespace CryptoExchange.Net.Sockets
sw.Stop();
if (sw.ElapsedMilliseconds > 500)
log.Write(LogVerbosity.Warning, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " +
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(LogVerbosity.Debug, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms");
log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms");
return handled;
}
catch (Exception ex)
{
log.Write(LogVerbosity.Error, $"Socket {Socket.Id} Exception during message processing\r\nException: {ex}\r\nData: {tokenData}");
log.Write(LogLevel.Error, $"Socket {Socket.Id} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
currentSubscription?.InvokeExceptionHandler(ex);
return false;
}
}
/// <summary>
/// Send data
/// Send data and wait for an answer
/// </summary>
/// <typeparam name="T">The data type</typeparam>
/// <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>
/// <returns></returns>
public virtual Task SendAndWait<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
{
var pending = new PendingRequest(handler, timeout);
lock (pendingRequests)
@ -262,7 +273,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Send data to the websocket
/// Send data over the websocket connection
/// </summary>
/// <typeparam name="T">The type of the object to send</typeparam>
/// <param name="obj">The object to send</param>
@ -276,12 +287,12 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Send string data to the websocket
/// Send string data over the websocket connection
/// </summary>
/// <param name="data">The data to send</param>
public virtual void Send(string data)
{
log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} sending data: {data}");
log.Write(LogLevel.Debug, $"Socket {Socket.Id} sending data: {data}");
Socket.Send(data);
}
@ -297,11 +308,12 @@ namespace CryptoExchange.Net.Sockets
Socket.Reconnecting = true;
log.Write(LogVerbosity.Info, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}");
log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}");
Task.Run(async () =>
{
while (ShouldReconnect)
{
// Wait a bit before attempting reconnect
await Task.Delay(socketClient.ReconnectInterval).ConfigureAwait(false);
if (!ShouldReconnect)
{
@ -311,20 +323,21 @@ namespace CryptoExchange.Net.Sockets
}
Socket.Reset();
if (!await Socket.Connect().ConfigureAwait(false))
if (!await Socket.ConnectAsync().ConfigureAwait(false))
{
log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} failed to reconnect");
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect");
continue;
}
// Successfully reconnected
var time = DisconnectTime;
DisconnectTime = null;
log.Write(LogVerbosity.Info, $"Socket {Socket.Id} reconnected after {DateTime.UtcNow - time}");
log.Write(LogLevel.Information, $"Socket {Socket.Id} reconnected after {DateTime.UtcNow - time}");
var reconnectResult = await ProcessReconnect().ConfigureAwait(false);
var reconnectResult = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectResult)
await Socket.Close().ConfigureAwait(false);
await Socket.CloseAsync().ConfigureAwait(false);
else
{
if (lostTriggered)
@ -342,11 +355,11 @@ namespace CryptoExchange.Net.Sockets
}
else
{
log.Write(LogVerbosity.Info, $"Socket {Socket.Id} closed");
// No reconnecting needed
log.Write(LogLevel.Information, $"Socket {Socket.Id} closed");
if (socketClient.sockets.ContainsKey(Socket.Id))
socketClient.sockets.TryRemove(Socket.Id, out _);
Socket.Dispose();
Closed?.Invoke();
}
}
@ -356,29 +369,32 @@ namespace CryptoExchange.Net.Sockets
await Task.Run(() => ConnectionRestored?.Invoke(disconnectTime.HasValue ? DateTime.UtcNow - disconnectTime.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false);
}
private async Task<bool> ProcessReconnect()
private async Task<bool> ProcessReconnectAsync()
{
if (Authenticated)
{
var authResult = await socketClient.AuthenticateSocket(this).ConfigureAwait(false);
// If we reconnected a authenticated connection we need to re-authenticate
var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
if (!authResult)
{
log.Write(LogVerbosity.Info, "Authentication failed on reconnected socket. Disconnecting and reconnecting.");
log.Write(LogLevel.Information, $"Socket {Socket.Id} authentication failed on reconnected socket. Disconnecting and reconnecting.");
return false;
}
log.Write(LogVerbosity.Debug, "Authentication succeeded on reconnected socket.");
log.Write(LogLevel.Debug, $"Socket {Socket.Id} authentication succeeded on reconnected socket.");
}
List<SocketSubscription> handlerList;
lock (handlersLock)
handlerList = handlers.Where(h => h.Request != null).ToList();
// Get a list of all subscriptions on the socket
List<SocketSubscription> subscriptionList;
lock (subscriptionLock)
subscriptionList = subscriptions.Where(h => h.Request != null).ToList();
var success = true;
var taskList = new List<Task>();
foreach (var handler in handlerList)
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
foreach (var subscription in subscriptionList)
{
var task = socketClient.SubscribeAndWait(this, handler.Request!, handler).ContinueWith(t =>
var task = socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription).ContinueWith(t =>
{
if (!t.Result)
success = false;
@ -386,14 +402,14 @@ namespace CryptoExchange.Net.Sockets
taskList.Add(task);
}
Task.WaitAll(taskList.ToArray());
await Task.WhenAll(taskList).ConfigureAwait(false);
if (!success)
{
log.Write(LogVerbosity.Debug, "Resubscribing all subscriptions failed on reconnected socket. Disconnecting and reconnecting.");
log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket. Disconnecting and reconnecting.");
return false;
}
log.Write(LogVerbosity.Debug, "All subscription successfully resubscribed on reconnected socket.");
log.Write(LogLevel.Debug, $"Socket {Socket.Id} all subscription successfully resubscribed on reconnected socket.");
return true;
}
@ -401,36 +417,39 @@ namespace CryptoExchange.Net.Sockets
/// Close the connection
/// </summary>
/// <returns></returns>
public async Task Close()
public async Task CloseAsync()
{
Connected = false;
ShouldReconnect = false;
if (socketClient.sockets.ContainsKey(Socket.Id))
socketClient.sockets.TryRemove(Socket.Id, out _);
await Socket.Close().ConfigureAwait(false);
await Socket.CloseAsync().ConfigureAwait(false);
Socket.Dispose();
}
/// <summary>
/// Close the subscription
/// 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 Close(SocketSubscription subscription)
public async Task CloseAsync(SocketSubscription subscription)
{
if (!Socket.IsOpen)
return;
if (subscription.Confirmed)
await socketClient.Unsubscribe(this, subscription).ConfigureAwait(false);
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
var shouldCloseWrapper = false;
lock (handlersLock)
shouldCloseWrapper = handlers.Count(r => r.UserSubscription && subscription != r) == 0;
var shouldCloseConnection = false;
lock (subscriptionLock)
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
if (shouldCloseWrapper)
await Close().ConfigureAwait(false);
if (shouldCloseConnection)
await CloseAsync().ConfigureAwait(false);
lock (handlersLock)
handlers.Remove(subscription);
lock (subscriptionLock)
subscriptions.Remove(subscription);
}
}

View File

@ -1,5 +1,4 @@
using System;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Sockets
{
@ -16,7 +15,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Message handlers for this subscription. Should return true if the message is handled and should not be distributed to the other handlers
/// </summary>
public Action<SocketConnection, JToken> MessageHandler { get; set; }
public Action<MessageEvent> MessageHandler { get; set; }
/// <summary>
/// Request object
@ -36,7 +35,7 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool Confirmed { get; set; }
private SocketSubscription(object? request, string? identifier, bool userSubscription, Action<SocketConnection, JToken> dataHandler)
private SocketSubscription(object? request, string? identifier, bool userSubscription, Action<MessageEvent> dataHandler)
{
UserSubscription = userSubscription;
MessageHandler = dataHandler;
@ -52,7 +51,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="dataHandler"></param>
/// <returns></returns>
public static SocketSubscription CreateForRequest(object request, bool userSubscription,
Action<SocketConnection, JToken> dataHandler)
Action<MessageEvent> dataHandler)
{
return new SocketSubscription(request, null, userSubscription, dataHandler);
}
@ -65,7 +64,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="dataHandler"></param>
/// <returns></returns>
public static SocketSubscription CreateForIdentifier(string identifier, bool userSubscription,
Action<SocketConnection, JToken> dataHandler)
Action<MessageEvent> dataHandler)
{
return new SocketSubscription(null, identifier, userSubscription, dataHandler);
}

View File

@ -4,7 +4,7 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Subscription
/// Subscription to a data stream
/// </summary>
public class UpdateSubscription
{
@ -21,7 +21,9 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting
/// 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.
/// </summary>
public event Action<TimeSpan> ConnectionRestored
{
@ -30,7 +32,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Event when the connection to the server is paused. No operations can be performed while paused
/// Event when the connection to the server is paused based on a server indication. No operations can be performed while paused
/// </summary>
public event Action ActivityPaused
{
@ -39,7 +41,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Event when the connection to the server is unpaused
/// Event when the connection to the server is unpaused after being paused
/// </summary>
public event Action ActivityUnpaused
{
@ -48,7 +50,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Event when an exception happened
/// Event when an exception happens during the handling of the data
/// </summary>
public event Action<Exception> Exception
{
@ -64,8 +66,8 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// ctor
/// </summary>
/// <param name="connection"></param>
/// <param name="subscription"></param>
/// <param name="connection">The socket connection the subscription is on</param>
/// <param name="subscription">The subscription</param>
public UpdateSubscription(SocketConnection connection, SocketSubscription subscription)
{
this.connection = connection;
@ -76,18 +78,18 @@ namespace CryptoExchange.Net.Sockets
/// Close the subscription
/// </summary>
/// <returns></returns>
public async Task Close()
public Task CloseAsync()
{
await connection.Close(subscription).ConfigureAwait(false);
return connection.CloseAsync(subscription);
}
/// <summary>
/// Close the socket to cause a reconnect
/// </summary>
/// <returns></returns>
internal Task Reconnect()
internal Task ReconnectAsync()
{
return connection.Socket.Close();
return connection.Socket.CloseAsync();
}
}
}

View File

@ -5,20 +5,20 @@ using CryptoExchange.Net.Logging;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Factory implementation
/// Default weboscket factory implementation
/// </summary>
public class WebsocketFactory : IWebsocketFactory
{
/// <inheritdoc />
public IWebsocket CreateWebsocket(Log log, string url)
{
return new BaseSocket(log, url);
return new CryptoExchangeWebSocketClient(log, url);
}
/// <inheritdoc />
public IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
{
return new BaseSocket(log, url, cookies, headers);
return new CryptoExchangeWebSocketClient(log, url, cookies, headers);
}
}
}

411
README.md
View File

@ -1,17 +1,10 @@
# CryptoExchange.Net
# CryptoExchange.Net
![Build status](https://travis-ci.org/JKorf/CryptoExchange.Net.svg?branch=master) ![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg) ![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg)
![Build status](https://travis-ci.org/JKorf/CryptoExchange.Net.svg?branch=master)
CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections.
A base library for easy implementation of cryptocurrency API's. Include:
* REST API calls and error handling
* Websocket subscriptions, error handling and automatic reconnecting
* Order book implementations automatically synchronizing and updating
* Automatic rate limiting
**If you think something is broken, something is missing or have any questions, please open an [Issue](https://github.com/JKorf/CryptoExchange.Net/issues)**
---
## Implementations
By me:
<table>
<tr>
<td><a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Bittrex.Net/Icon/icon.png?raw=true"></a>
@ -22,10 +15,6 @@ A base library for easy implementation of cryptocurrency API's. Include:
<br />
<a href="https://github.com/JKorf/Bitfinex.Net">Bitfinex</a>
</td>
<td><a href="https://github.com/JKorf/Binance.Net"><img src="https://github.com/JKorf/Binance.Net/blob/master/Binance.Net/Icon/icon.png?raw=true"></a>
<br />
<a href="https://github.com/JKorf/Binance.Net">Binance</a>
</td>
<td><a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/CoinEx.Net/Icon/icon.png?raw=true"></a>
<br />
<a href="https://github.com/JKorf/CoinEx.Net">CoinEx</a>
@ -44,8 +33,7 @@ A base library for easy implementation of cryptocurrency API's. Include:
</td>
</tr>
</table>
Implementations from third parties
By third parties:
<table>
<tr>
<td><a href="https://github.com/Zaliro/Switcheo.Net"><img src="https://github.com/Zaliro/Switcheo.Net/blob/master/Resources/switcheo-coin.png?raw=true"></a>
@ -56,7 +44,7 @@ Implementations from third parties
<br />
<a href="https://github.com/ridicoulous/LiquidQuoine.Net">Liquid</a>
</td>
<td><a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png"></a>
<td><a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>
<br />
<a href="https://github.com/ridicoulous/Bitmex.Net">Bitmex</a>
</td>
@ -80,7 +68,7 @@ Implementations from third parties
<br />
<a href="https://github.com/burakoner/BtcTurk.Net">BtcTurk</a>
</td>
<td><a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/main/Thodex.Net/Icon/icon.png?raw=true"></a>
<td><a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>
<br />
<a href="https://github.com/burakoner/Thodex.Net">Thodex</a>
</td>
@ -95,105 +83,80 @@ Implementations from third parties
</tr>
</table>
Planned implementations (no timeline or specific order):
* Bitstamp
* CoinFalcon
* Binance DEX
## Discord
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
## Donations
Donations are greatly appreciated and a motivation to keep improving.
I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me.
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
## Discord
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). For discussion and/or questions around the CryptoExchange.Net and implementation libraries, feel free to join.
## Implementation usage
### Clients
The CryptoExchange.Net library offers 2 base clients which should be implemented in each implementation library. The `RestClient` and the `SocketClient`.
## Usage
Most API methods are available in two flavors, sync and async, see the example using the `BinanceClient`:
**RestClient**
The `RestClient`, as the name suggests, handles requests to the exchange REST API. Typically the `RestClient` implementation name is [ExchangeName]Client. So for the Binance exchange this would be called the `BinanceClient`, and for Bittrex the client type name is `BittrexClient`.
The `RestClient` implementations can be used in either a `using` or with a static instance:
````C#
public void NonAsyncMethod()
using (var binanceClient = new BinanceClient())
{
using(var client = new BinanceClient())
{
var result = client.Spot.Market.Get24HPrices();
}
var exchangeInfoResult = binanceClient.Spot.System.GetExchangeInfoAsync();
}
public async Task AsyncMethod()
{
using(var client = new BinanceClient())
{
var result2 = await client.Spot.Market.Get24HPricesAsync();
}
}
````
## Response handling
All API requests will respond with a (Web)CallResult object. This object contains whether the call was successful, the data returned from the call and an error if the call wasn't successful. As such, one should always check the Success flag when processing a response.
For example:
```C#
using(var client = new BinanceClient())
{
var result = client.Spot.System.GetServerTime();
if (result.Success)
Console.WriteLine($"Server time: {result.Data}");
else
Console.WriteLine($"Error: {result.Error}");
}
```
## Options & Authentication
The default behavior of the clients can be changed by providing options to the constructor, or using the `SetDefaultOptions` before creating a new client. Api credentials can be provided in the options.
Credentials can be provided 2 ways:
* Providing key and secret:
````
or
````C#
BinanceClient.SetDefaultOptions(new BinanceClientOptions
{
ApiCredentials = new ApiCredentials("apiKey", "apiSecret")
});
var client = new BinanceClient();
var exchangeInfoResult = client.Spot.System.GetExchangeInfoAsync();
````
* Providing a (file)stream containing the key/secret
If you're opting for the `using` syntax, a `HttpClient` should be provided in the client options to prevent each client creating it's own `HttpClient` instance.
Calls made on the `RestClient` will return a `WebCallResult<T>` object which will contain the following properties:
|Property|Description|Available when
|---|---|---|
|`Success`|Whether or not the call was successfully executed.|Always
|`Data`|The data the server sent us as response.| When `Success` is `true`
|`Error`|Details on the error that happened during the call.| When `Success` is `false`
|`OriginalData`|The originally received Json data which was received before being deserialized into an object.| When `OutputOriginalData` is enabled in the client options
|`ResponseStatusCode`|The Http status code received as answer on the request.|Always
|`ResponseHeaders`|The headers received in the response.|Always
The `RestClient` implementation should implement the `IExchangeClient` interface, which offers some basic methods for interacting with an exchange without having to know the implementation.
**SocketClient**
The `SocketClient` can be used to connect to websocket streams offered by the API, and receive callbacks whenever new data is received. `SocketClient` implementations are typically named [ExchangeName]SocketClient, so in case of Binance this would be `BinanceSocketClient`.
To use the `SocketClient` to connect to a stream simply call the `SubscribeXXX` method for the data you're interested in and pass in a delegate for handling updates. For example, to subscribe to the Binance ticker stream call `SubscribeToAllSymbolTickerUpdatesAsync` method:
````C#
using (var stream = File.OpenRead("/path/to/credential-file"))
{
BinanceClient.SetDefaultOptions(new BinanceClientOptions
{
ApiCredentials = new ApiCredentials(stream)
});
}
var socketClient = new BinanceSocketClient();
var subscribeResult = socketClient.Spot.SubscribeToAllSymbolTickerUpdatesAsync(data => {
// Handle updates received here
});
````
Note that when using a file it can provide credentials for multiple exchanges by providing the identifierKey and identifierSecret parameters:
````
// File content:
{
"binanceKey": "actualBinanceApiKey",
"binanceSecret": "actualBinanceApiSecret",
"bittrexKey": "actualBittrexApiKey",
"bittrexSecret": "actualBittrexApiSecret",
}
or
````C#
var socketClient = new BinanceSocketClient();
var subscribeResult = socketClient.Spot.SubscribeToAllSymbolTickerUpdatesAsync(HandleData);
// Loading:
using (var stream = File.OpenRead("/path/to/credential-file"))
private void HandleData(DataEvent<IEnumerable<BinanceTick>> data)
{
BinanceClient.SetDefaultOptions(new BinanceClientOptions
{
ApiCredentials = new ApiCredentials(stream, "binanceKey", "binanceSecret")
});
BittrexClient.SetDefaultOptions(new BittrexClientOptions
{
ApiCredentials = new ApiCredentials(stream, "bittrexKey", "bittrexSecret")
});
// Handle updates received here
}
````
## Websockets
Most implementations have a websocket client. The client will manage the websocket connection for you, all you have to do is call a subscribe method. The client will automatically handle reconnecting when losing a connection.
Make sure to check the result of the subscribe call to ensure it was successful. The Subscribe methods will return a `CallResult<UpdateSubscription>` object with the following properties:
|Property|Description|Available when
|---|---|---|
|`Success`|Whether or not the call was successfully executed.|Always
|`Data`|The `UpdateSubscription` object for this stream. This can be used to for listening to connection changed events and unsubscribing | When `Success` is `true`
|`Error`|Details on the error that happened during the subscription. | When `Success` is `false`
When using a subscribe method it will return an `UpdateSubscription` object. This object has 3 events: ConnectionLost/ConnectionRestored and Exception. Use the connection lost/restored to be notified when the socket has lost it's connection and when it was reconnected. The Exception event is thrown when there was an exception within the data handling callback.
To unsubscribe use the client.Unsubscribe method and pass the UpdateSubscription received when subscribing:
To unsubscribe from a stream `Unsubscribe()` method can be used, with the `UpdateSubscription` received in the `SubscribeXXX` call as parameter, or use the `UnsubscribeAll()` method to close all subscriptions:
````C#
// Subscribe
var client = new BinanceSocketClient();
@ -202,15 +165,59 @@ var subResult = client.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {});
// Unsubscribe
client.Unsubscribe(subResult.Data);
````
To unsubscribe all subscriptions the `client.UnsubscribeAll()` method can be used.
The `SocketClient` handles connection management to the server internally and will close a connection if there are no more subscriptions on that connection.
## Order books
The library implementations provide a `SymbolOrderBook` implementation. This implementation can be used to keep an updated order book without having to think about synchronizing it. This example is from the Binance.Net library,
but the implementation is similar for each library:
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
### Client options
Options for a client can be provided in the constructor or using the static SetDefaultOptions method.
````C#
var client = new BinanceClient(new BinanceClientOptions()
{
});
````
or
````C#
BinanceClient.SetDefaultOptions(new BinanceClientOptions()
{
});
````
When providing options in the constructor the options will only apply for that specific client. When using the `SetDefaultOptions` method the options will be applied to any client created after that call which doesn't have any options provided to it via the constructor. Providing options in the constructor means any options set using the `SetDefaultOptions` method will be reset to default unless overwritten in the provided options.
**Options for all clients**
| Property | Description | Default |
| ----------- | ----------- | ---------|
| `LogWriters`| A list of `ILogger`s to handle log messages. | `new List<ILogger> { new DebugLogger() }` |
| `LogLevel`| The minimum log level before passing messages to the `LogWriters`. Messages with a more verbose level than the one specified here will be ignored. Setting this to `null` will pass all messages to the `LogWriters`.| `LogLevel.Information`
|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update. | `false`
|`BaseAddress`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example.| Depends on implementation
|`ApiCredentials`| The API credentials to use for accessing protected endpoints. Typically a key/secret combination.| `null`
|`Proxy`|The proxy to use for connecting to the API.| `null`
**Options for RestClients**
| Property | Description | Default |
| ----------- | ----------- | ---------|
| `RateLimiters`| A list of `IRateLimiter`s to use.| `new List<IRateLimiter>()` |
| `RateLimitingBehaviour`| What should happen when a rate limit is reached.| `RateLimitingBehaviour.Wait` |
| `RequestTimeout`| The time out to use for requests.| `TimeSpan.FromSeconds(30)` |
| `HttpClient`| The `HttpClient` instance to use for making requests. When creating multiple `RestClient` instances a single `HttpClient` should be provided to prevent each client instance from creating its own. *[WARNING] When providing the `HttpClient` instance in the options both the `RequestTimeout` and `Proxy` client options will be ignored and should be set on the provided `HttpClient` instance.*| `null` |
**Options for SocketClients**
| Property | Description | Default |
| ----------- | ----------- | ---------|
|`AutoReconnect`|Whether or not the socket should automatically reconnect when disconnected.|`true`
|`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)`
|`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)`
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout)
|`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges 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.| Depends on implementation
### Order book
The library implementations provide a `SymbolOrderBook` implementation. This implementation can be used to keep an updated order book without having to think about synchronization. This example is from the Binance.Net library, but the implementation is similar for each library:
````C#
var orderBook = new BinanceSymbolOrderBook("BTCUSDT", new BinanceOrderBookOptions(20));
orderBook.OnStatusChange += (oldStatus, newStatus) => Console.WriteLine($"Book state changed from {oldStatus} to {newStatus}");
orderBook.OnOrderBookUpdate += (changedBids, changedAsks) => Console.WriteLine("Book updated");
orderBook.OnOrderBookUpdate += ((changedBids, changedAsks)) => Console.WriteLine("Book updated");
var startResult = await orderBook.StartAsync();
if(!startResult.Success)
{
@ -218,85 +225,171 @@ if(!startResult.Success)
return;
}
var status = orderBook.Status; // The current status. Note that the order book is only current when the status is Synced
var status = orderBook.Status; // The current status. Note that the order book is only up to date when the status is Synced
var askCount = orderBook.AskCount; // The current number of asks in the book
var bidCount = orderBook.BidCount; // The current number of bids in the book
var asks = orderBook.Asks; // All asks
var bids = orderBook.Bids; // All bids
var bestBid = orderBook.BestBid; // The best bid available in the book
var bestAsk = orderBook.BestAsk; // The best ask available in the book
````
The order book will automatically reconnect when the connection is lost and resync if it detects the sequence is off. Make sure to check the Status property to see it the book is currently in sync.
The order book will automatically reconnect when the connection is lost and resync if it detects that it is out of sync. Make sure to check the Status property to see it the book is currently in sync.
To stop synchronizing an order book use the `Stop` method.
## Shared IExchangeClient interface
Most implementations have an implementation of the `IExchangeClient` interface. It is a shared interface, which makes it easier to re-use the same code for multiple exchanges. It offers basic functionality only, for exchange specific calls use the actual client interface, for example `IBinanceClient` or `IBitfinexClient`.
## Helper methods
The static `ExchangeHelpers` class offers some helper methods for adjusting value like rounding and adjusting values to fit a certain step size or precision.
The IExchangeClient interface supports the following methods:
## Creating an implementation
Implementations should implement at least the following:
**[Exchange]Client based on the RestClient base class**
Containing calls to the different endpoints, internally using the `SendRequest<T>` method of the `RestClient`.
`string GetSymbolName(string baseAsset, string quoteAsset);` - Based on a base and quote asset return the symbol name for the exchange.
**[Exchange]SocketClient based on the SocketClient base class**
Containing methods to subscribe to different streams using the `Subscribe<T>` method of the `SocketClient`.
Implement exchange specific handling of requests/messages by overriding these methods:
`HandleQueryResponse`: Check if the data received from the websocket matches the sent query.
`HandleSubscriptionResponse`: Check if the data received from the websocket matches the subscription request.
`MessageMatchesHandler`: Check if the data received from the websocket matches the handler/subscription.
`AuthenticateSocket`: Authenticate the connection to be able to subscribe to protected streams.
`Unsubscribe`: Unsubscribe from a stream, typically by sending an Unsubscribe message.
`GetSymbolsAsync();` - Get a list of supported symbols.
**[Exchange]SymbolOrderBook based on the SymbolOrderBook base class**
An implementation of an automatically synchronized order book. Implement exchange specific behavior by implementing these methods:
`DoStart`: Start the order book and sync process
`DoResync`: Resync the order book after a reconnection
`DoReset`: Reset the state of the orderbook, called when the connection is lost.
`DoChecksum`: [Optional] Validate the order book with a checksum.
`GetTickersAsync();` - Get a list of tickers (High/Low/volume data for the last 24 hours) for all symbols.
**[Exchange]AuthenticationProvider**
An implementation of the AuthenticationProvider base class. Should contain the logic for authenticating requests from the RestClient on protected endpoints. Override these methods as needed:
`AddAuthenticationToParameters`: Will be called before `AddAuthenticationToHeaders`, allows the implementation to add specific parameters to the request which are needed for protected endpoints.
`AddAuthenticationToHeaders`: Will be called after `AddAuthenticationToParameters`, allows the implementation to add specific headers to the request message.
If you have any issues or questions regarding implementing an exchange using CryptoExchange.Net hop into the Discord or open an issue.
`GetTickerAsync(string symbol);` - Get a specific ticker.
`GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);` - Get candlestick data for a symbol.
`GetOrderBookAsync(string symbol);` - Get the order book for a symbol.
`GetRecentTradesAsync(string symbol);` - Get a list of the most recent trades for a symbol.
`PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal quantity, decimal? price = null, string? accountId = null);` - \[Authenticated\] Place an order.
`GetOrderAsync(string orderId, string? symbol = null);` - \[Authenticated\] Get details on an order.
`GetTradesAsync(string orderId, string? symbol = null);` - \[Authenticated\] Get executed trades for an order.
`GetOpenOrdersAsync(string? symbol = null);` - \[Authenticated\] Get a list of open orders.
`GetClosedOrdersAsync(string? symbol = null);` - \[Authenticated\] Get a list of closed orders.
`CancelOrderAsync(string orderId, string? symbol = null);` - \[Authenticated\] Cancel an order.
`GetBalancesAsync(string? accountId = null);` - \[Authenticated\] Get a list of balances.
Example usage:
## FAQ
**I sometimes get NullReferenceException, what's wrong?**
You probably don't check the result status of a call and just assume the data is always there. `NullReferenceExecption`s will happen when you have code like this `var symbol = client.GetTickersAync().Result.Data.Symbol` because the `Data` property is null when the call fails. Instead check if the call is successful like this:
````C#
static async Task Main(string[] args)
var tickerResult = await client.GetTickersAync();
if(!tickerResult.Success)
{
var clients = new List<IExchangeClient>()
{
{ new BinanceClient(new BinanceClientOptions(){ LogVerbosity = LogVerbosity.Debug, ApiCredentials = new ApiCredentials("BinanceKey", "BinanceSecret") }) },
{ new BitfinexClient(new BitfinexClientOptions(){ LogVerbosity = LogVerbosity.Debug, ApiCredentials = new ApiCredentials("BitfinexKey", "BitfinexSecret") }) },
{ new BittrexClientV3(new BittrexClientOptions(){ LogVerbosity = LogVerbosity.Debug, ApiCredentials = new ApiCredentials("BittrexKey", "BittrexSecret") }) },
};
// Handle error
}
else
{
// Handle data, it is now safe to access the data
var symbol = tickerResult.Data.Symbol;
}
````
**The socket client stops sending updates after a little while**
You probably didn't keep a reference to the socket client and it got disposed.
Instead of subscribing like this:
````C#
private void SomeMethod()
{
var socketClient = new BinanceSocketClient();
socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
// Handle data
});
}
````
Subscribe like this:
````C#
private BinanceSocketClient _socketClient;
await Task.WhenAll(clients.Select(GetExchangeData));
Console.WriteLine("Done");
Console.ReadLine();
// .. rest of the class
private void SomeMethod()
{
if(_socketClient == null)
_socketClient = new BinanceSocketClient();
_socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
// Handle data
});
}
static async Task GetExchangeData(IExchangeClient client)
{
var symbols = await client.GetSymbolsAsync();
Console.WriteLine($"{((RestClient)client).ClientName}: {symbols.Data?.Count()} symbols returned, first = {symbols.Data?.First().CommonName}");
var balances = await client.GetBalancesAsync();
var btcBalance = balances.Data?.Where(b => b.CommonAsset.ToUpperInvariant() == "BTC").FirstOrDefault();
Console.WriteLine($"{((RestClient)client).ClientName}: {balances.Data?.Count()} balance returned, BTC balance = {btcBalance?.CommonTotal}");
var symbolName = client.GetSymbolName("BTC", "USDT");
var klines = await client.GetKlinesAsync(symbolName, TimeSpan.FromHours(1));
Console.WriteLine($"{((RestClient)client).ClientName}: {klines.Data?.Count()} klines returned, first klines @ {klines.Data?.First().CommonClose}");
}
````
## Release notes
* Version 4.0.0 - 12 Aug 2020
* Release version, summed up changes from previous beta releases:
* Removed `Websocket4Net` dependency in favor of a `ClientWebSocket` native implementation for websocket connections
* Socket events now always come wrapped in a `DataEvent<>` object which contain the timestamp of the data, and optionally the originally received json string
* Implemented usage of the `Microsoft.Extensions.Logging.Abstractions` `ILogger` interface instead of a custom implementation
* Added some properties to the `IExchangeClient` interface
* `ICommonOrder.CommonOrderTime`
* `ICommonOrder.CommonOrderStatus` enum
* `ICommonTrade.CommonTradeTime`
* Added `OnOrderPlaced` and `OnOrderCanceled` events on the `IExchangeClient` interface
* Added `ExchangeHelpers` static class for various helper methods
* Removed non-async methods due to too much overhead in development/maintainance
* If you were previously using non-async methods you can add `.Result` to the end of the async call to get the same result
* Added `Book` property to `SymbolOrderBook` for a book snapshot
* Added `CalculateAverageFillPrice` to `SymbolOrderBook` to calculate the average fill price for an order with the current order book state
* Various fixes
* Version 4.0.0-beta15 - 12 Aug 2021
* Conditional version Logging.Abstractions
* Version 4.0.0-beta14 - 09 Aug 2021
* Fix for bug in processing order in SymbolOrderBook
* Version 4.0.0-beta13 - 31 Jul 2021
* Fix for socket connection
* Version 4.0.0-beta12 - 26 Jul 2021
* Fix for socket connection
* Version 4.0.0-beta11 - 09 Jul 2021
* Added CalculateAverageFillPrice to SymbolOrderBook
* Added Book property to SymbolOrderBook
* Added Async postfix to async methods
* Version 4.0.0-beta10 - 07 Jul 2021
* Updated BaseConverter to be case sensitive
* Added ExchangeHelpers class containing some helper methods
* Fixed responses not being logged on Trace log level
* Added some code docs
* Version 4.0.0-beta9 - 17 Jun 2021
* Small fixes
* Version 4.0.0-beta8 - 08 Jun 2021
* Fixed exception socket buffer size in .net framework
* Version 4.0.0-beta7 - 07 Jun 2021
* Added CommonOrderTime to IOrder
* Added OrderStatus enum for IOrder
* Added OnOrderPlaced and OnOrderCanceled events on IExchangeClient
* Added CommonTradeTime to ICommonTrade
* Version 4.0.0-beta6 - 01 jun 2021
* Some logging adjustments
* Fixed some async issues
* Version 4.0.0-beta5 - 26 May 2021
* Added DataEvent wrapper for socket updates
* Added optional original json output
* Changed logging implementation to use ILogger
* Version 4.0.0-beta4 - 06 mei 2021
* Added analyzers
* Fixed some warnings
* Version 4.0.0-beta3 - 30 Apr 2021
* Updated socket closing
* Version 4.0.0-beta2 - 30 apr 2021
* Fix for closing socket without timeout task
* Version 4.0.0-beta1 - 30 apr 2021
* Removed Websocket4Net dependency
* Added custom ClientWebSocket implementation
* Renamed handler -> subscription internally
* Renamed socket -> socketConenction when type is socketConnection
* Version 3.9.0 - 28 apr 2021
* Added optional JsonSerializer parameter to SendRequest to use during deserialization
* Fix for unhandled message warning when unsubscribing a socket subscription
@ -432,4 +525,4 @@ static async Task GetExchangeData(IExchangeClient client)
* Version 2.1.2 - 14 may 2019
* Added order book base class for easy implementation
* Added additional constructor to ApiCredentials to be able to read from file
* Added additional constructor to ApiCredentials to be able to read from file