mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0005534a95 | |||
| 11650f7c1a | |||
| a222bb3f02 | |||
| 6361c5ef25 | |||
| 892e8a4508 | |||
| 8336d373f3 | |||
| 71072680a8 | |||
| e13f105019 | |||
| 401577451e | |||
| 5c41ef1ee4 | |||
| ad614830d1 | |||
| 3365837338 | |||
| 66ac2972d6 | |||
| 0d3e05880a | |||
| 997e71f3b7 | |||
| b0fca4587d | |||
| c10671768d | |||
| 91e8123679 | |||
| 417cf2f9ac | |||
| 277be7ab9b | |||
| 45f3459f59 | |||
| 98dad4a8ed | |||
| 1e5f19271b | |||
| 8abeeb4cf0 | |||
| cae0cd9ead | |||
| 811574ae01 | |||
| 0ddecf7f8d | |||
| 5bcf50fb4d | |||
| 9f0654815d | |||
| 465e9f04f4 | |||
| 7c8cbfa4e2 | |||
| 4c79d13ff9 | |||
| c815fad135 | |||
| 41f17d0378 | |||
| 50715ff2f7 | |||
| ea9375d582 | |||
| 2cf3c93e5e | |||
| ca888d8e41 | |||
| 2040b1c175 | |||
| d451c18821 | |||
| c13dfa4461 | |||
| c2080ef75f | |||
| 6b252e8024 | |||
| d06bd5f176 | |||
| d55fc8da65 | |||
| 01184f2c5d | |||
| cadc93c2f0 | |||
| 2600a51461 | |||
| 9e6a86ba8b | |||
| c4430d63fa | |||
| f3e1cfef33 | |||
| cc3053719c | |||
| cd6907e601 | |||
| 8fe00693bd | |||
| fb90d1e015 | |||
| 4b44861e43 | |||
| e42ca4ab5a | |||
| 5b97f6dd67 | |||
| a9813ecb0a | |||
| c7069a4049 | |||
| 5683ae0b3c | |||
| 1c8cf5ac98 | |||
| ad7231ec56 | |||
| 7e4a607391 | |||
| 2d470d18e2 | |||
| cb9a766c3b | |||
| 94b8184f7b | |||
| 270ea06f24 | |||
| 536afa92da | |||
| 11c48b3341 | |||
| f514e172d7 |
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var logger = new TestStringLogger();
|
var logger = new TestStringLogger();
|
||||||
var client = new TestBaseClient(new BaseRestClientOptions()
|
var client = new TestBaseClient(new TestOptions()
|
||||||
{
|
{
|
||||||
LogWriters = new List<ILogger> { logger }
|
LogWriters = new List<ILogger> { logger }
|
||||||
});
|
});
|
||||||
@@ -56,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var logger = new TestStringLogger();
|
var logger = new TestStringLogger();
|
||||||
var options = new BaseRestClientOptions()
|
var options = new TestOptions()
|
||||||
{
|
{
|
||||||
LogWriters = new List<ILogger> { logger }
|
LogWriters = new List<ILogger> { logger }
|
||||||
};
|
};
|
||||||
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestBaseClient();
|
var client = new TestBaseClient();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Deserialize<object>("{\"testProperty\": 123}");
|
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(result.Success);
|
Assert.IsTrue(result.Success);
|
||||||
@@ -91,7 +91,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestBaseClient();
|
var client = new TestBaseClient();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Deserialize<object>("{\"testProperty\": 123");
|
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></packagereference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
|
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||||
<packagereference Include="NUnit3TestAdapter" Version="4.2.0"></packagereference>
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestClientOptions: BaseRestClientOptions
|
public class TestClientOptions: ClientOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default options for the futures client
|
/// Default options for the futures client
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Request<TestObject>().Result;
|
var result = client.Api1.Request<TestObject>().Result;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsTrue(result.Success);
|
Assert.IsTrue(result.Success);
|
||||||
@@ -43,7 +43,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetResponse("{\"property\": 123", out _);
|
client.SetResponse("{\"property\": 123", out _);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Request<TestObject>().Result;
|
var result = client.Api1.Request<TestObject>().Result;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
@@ -58,7 +58,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = await client.Request<TestObject>();
|
var result = await client.Api1.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
@@ -73,7 +73,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = await client.Request<TestObject>();
|
var result = await client.Api1.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
@@ -91,7 +91,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = await client.Request<TestObject>();
|
var result = await client.Api2.Request<TestObject>();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(result.Success);
|
Assert.IsFalse(result.Success);
|
||||||
@@ -112,9 +112,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
BaseAddress = "http://test.address.com",
|
BaseAddress = "http://test.address.com",
|
||||||
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
|
||||||
},
|
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
||||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||||
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
|
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||||
@@ -148,7 +148,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
client.SetResponse("{}", out var request);
|
client.SetResponse("{}", out var request);
|
||||||
|
|
||||||
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "TestParam1", "Value1" },
|
{ "TestParam1", "Value1" },
|
||||||
{ "TestParam2", 2 },
|
{ "TestParam2", 2 },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
@@ -19,17 +20,17 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
//act
|
//act
|
||||||
var client = new TestSocketClient(new TestOptions()
|
var client = new TestSocketClient(new TestOptions()
|
||||||
{
|
{
|
||||||
SubOptions = new RestApiClientOptions
|
SubOptions = new SocketApiClientOptions
|
||||||
{
|
{
|
||||||
BaseAddress = "http://test.address.com"
|
BaseAddress = "http://test.address.com",
|
||||||
},
|
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
||||||
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6);
|
Assert.IsTrue(client.SubClient.Options.ReconnectInterval.TotalSeconds == 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
@@ -42,7 +43,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
socket.CanConnect = canConnect;
|
socket.CanConnect = canConnect;
|
||||||
|
|
||||||
//act
|
//act
|
||||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket));
|
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new Log(""), client.SubClient, socket, null));
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.IsTrue(connectResult.Success == canConnect);
|
Assert.IsTrue(connectResult.Success == canConnect);
|
||||||
@@ -52,20 +53,26 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
var client = new TestSocketClient(new TestOptions() {
|
||||||
|
SubOptions = new SocketApiClientOptions
|
||||||
|
{
|
||||||
|
ReconnectInterval = TimeSpan.Zero,
|
||||||
|
},
|
||||||
|
LogLevel = LogLevel.Debug
|
||||||
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.ShouldReconnect = true;
|
socket.ShouldReconnect = true;
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
JToken result = null;
|
JToken result = null;
|
||||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||||
{
|
{
|
||||||
result = messageEvent.JsonData;
|
result = messageEvent.JsonData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
}));
|
}));
|
||||||
client.ConnectSocketSub(sub);
|
client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage("{\"property\": 123}");
|
socket.InvokeMessage("{\"property\": 123}");
|
||||||
@@ -80,20 +87,27 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
var client = new TestSocketClient(new TestOptions() {
|
||||||
|
SubOptions = new SocketApiClientOptions
|
||||||
|
{
|
||||||
|
ReconnectInterval = TimeSpan.Zero,
|
||||||
|
OutputOriginalData = enabled
|
||||||
|
},
|
||||||
|
LogLevel = LogLevel.Debug,
|
||||||
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.ShouldReconnect = true;
|
socket.ShouldReconnect = true;
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
string original = null;
|
||||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||||
{
|
{
|
||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
}));
|
}));
|
||||||
client.ConnectSocketSub(sub);
|
client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage("{\"property\": 123}");
|
socket.InvokeMessage("{\"property\": 123}");
|
||||||
@@ -103,44 +117,25 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public void DisconnectedSocket_Should_Reconnect()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
bool reconnected = false;
|
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
|
||||||
var socket = client.CreateSocket();
|
|
||||||
socket.ShouldReconnect = true;
|
|
||||||
socket.CanConnect = true;
|
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
|
||||||
var sub = new SocketConnection(client, null, socket);
|
|
||||||
sub.ShouldReconnect = true;
|
|
||||||
client.ConnectSocketSub(sub);
|
|
||||||
var rstEvent = new ManualResetEvent(false);
|
|
||||||
sub.ConnectionRestored += (a) =>
|
|
||||||
{
|
|
||||||
reconnected = true;
|
|
||||||
rstEvent.Set();
|
|
||||||
};
|
|
||||||
|
|
||||||
// act
|
|
||||||
socket.InvokeClose();
|
|
||||||
rstEvent.WaitOne(1000);
|
|
||||||
|
|
||||||
// assert
|
|
||||||
Assert.IsTrue(reconnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
var client = new TestSocketClient(new TestOptions()
|
||||||
|
{
|
||||||
|
SubOptions = new SocketApiClientOptions
|
||||||
|
{
|
||||||
|
ReconnectInterval = TimeSpan.Zero,
|
||||||
|
},
|
||||||
|
LogLevel = LogLevel.Debug
|
||||||
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||||
client.ConnectSocketSub(sub);
|
client.SubClient.ConnectSocketSub(sub);
|
||||||
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||||
|
var ups = new UpdateSubscription(sub, us);
|
||||||
|
sub.AddSubscription(us);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAsync(ups).Wait();
|
client.UnsubscribeAsync(ups).Wait();
|
||||||
@@ -153,15 +148,22 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
var client = new TestSocketClient(new TestOptions()
|
||||||
|
{
|
||||||
|
SubOptions = new SocketApiClientOptions
|
||||||
|
{
|
||||||
|
ReconnectInterval = TimeSpan.Zero,
|
||||||
|
},
|
||||||
|
LogLevel = LogLevel.Debug
|
||||||
|
});
|
||||||
var socket1 = client.CreateSocket();
|
var socket1 = client.CreateSocket();
|
||||||
var socket2 = client.CreateSocket();
|
var socket2 = client.CreateSocket();
|
||||||
socket1.CanConnect = true;
|
socket1.CanConnect = true;
|
||||||
socket2.CanConnect = true;
|
socket2.CanConnect = true;
|
||||||
var sub1 = new SocketConnection(client, null, socket1);
|
var sub1 = new SocketConnection(new Log(""), client.SubClient, socket1, null);
|
||||||
var sub2 = new SocketConnection(client, null, socket2);
|
var sub2 = new SocketConnection(new Log(""), client.SubClient, socket2, null);
|
||||||
client.ConnectSocketSub(sub1);
|
client.SubClient.ConnectSocketSub(sub1);
|
||||||
client.ConnectSocketSub(sub2);
|
client.SubClient.ConnectSocketSub(sub2);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAllAsync().Wait();
|
client.UnsubscribeAllAsync().Wait();
|
||||||
@@ -175,13 +177,20 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void FailingToConnectSocket_Should_ReturnError()
|
public void FailingToConnectSocket_Should_ReturnError()
|
||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
var client = new TestSocketClient(new TestOptions()
|
||||||
|
{
|
||||||
|
SubOptions = new SocketApiClientOptions
|
||||||
|
{
|
||||||
|
ReconnectInterval = TimeSpan.Zero,
|
||||||
|
},
|
||||||
|
LogLevel = LogLevel.Debug
|
||||||
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = false;
|
socket.CanConnect = false;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub1 = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var connectResult = client.ConnectSocketSub(sub);
|
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.IsFalse(connectResult.Success);
|
Assert.IsFalse(connectResult.Success);
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
public class TestBaseClient: BaseClient
|
public class TestBaseClient: BaseClient
|
||||||
{
|
{
|
||||||
public TestBaseClient(): base("Test", new BaseClientOptions())
|
public TestSubClient SubClient { get; }
|
||||||
|
|
||||||
|
public TestBaseClient(): base("Test", new TestOptions())
|
||||||
{
|
{
|
||||||
|
SubClient = AddApiClient(new TestSubClient(new TestOptions(), new RestApiClientOptions()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
public TestBaseClient(ClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +27,20 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
log.Write(verbosity, data);
|
log.Write(verbosity, data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public CallResult<T> Deserialize<T>(string data)
|
public class TestSubClient : RestApiClient
|
||||||
|
{
|
||||||
|
public TestSubClient(ClientOptions options, RestApiClientOptions apiOptions) : base(new Log(""), options, apiOptions)
|
||||||
{
|
{
|
||||||
return Deserialize<T>(data, null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
|
||||||
|
|
||||||
|
public override TimeSpan? GetTimeOffset() => null;
|
||||||
|
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||||
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestAuthProvider : AuthenticationProvider
|
public class TestAuthProvider : AuthenticationProvider
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -28,7 +29,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
Api1 = new TestRestApi1Client(exchangeOptions);
|
Api1 = new TestRestApi1Client(exchangeOptions);
|
||||||
Api2 = new TestRestApi2Client(exchangeOptions);
|
Api2 = new TestRestApi2Client(exchangeOptions);
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetResponse(string responseData, out IRequest requestObj)
|
public void SetResponse(string responseData, out IRequest requestObj)
|
||||||
@@ -50,7 +50,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||||
|
|
||||||
var factory = Mock.Get(RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||||
{
|
{
|
||||||
@@ -58,6 +58,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
})
|
})
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
|
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||||
|
{
|
||||||
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
|
request.Setup(a => a.Method).Returns(method);
|
||||||
|
})
|
||||||
|
.Returns(request.Object);
|
||||||
requestObj = request.Object;
|
requestObj = request.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +80,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
|
.Returns(request.Object);
|
||||||
|
|
||||||
|
|
||||||
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
@@ -94,27 +108,33 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
request.Setup(c => c.GetHeaders()).Returns(headers);
|
||||||
|
|
||||||
var factory = Mock.Get(RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
{
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
}
|
.Returns(request.Object);
|
||||||
|
|
||||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
|
||||||
{
|
|
||||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi1Client : RestApiClient
|
public class TestRestApi1Client : RestApiClient
|
||||||
{
|
{
|
||||||
public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options)
|
public TestRestApi1Client(TestClientOptions options): base(new Log(""), options, options.Api1Options)
|
||||||
{
|
{
|
||||||
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||||
@@ -122,7 +142,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
ParameterPositions[method] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan GetTimeOffset()
|
public override TimeSpan? GetTimeOffset()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -135,7 +155,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TimeSyncInfo GetTimeSyncInfo()
|
public override TimeSyncInfo GetTimeSyncInfo()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -143,12 +163,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient
|
public class TestRestApi2Client : RestApiClient
|
||||||
{
|
{
|
||||||
public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options)
|
public TestRestApi2Client(TestClientOptions options) : base(new Log(""), options, options.Api2Options)
|
||||||
{
|
{
|
||||||
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan GetTimeOffset()
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
|
{
|
||||||
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Error ParseErrorResponse(JToken error)
|
||||||
|
{
|
||||||
|
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TimeSpan? GetTimeOffset()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -161,7 +191,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TimeSyncInfo GetTimeSyncInfo()
|
public override TimeSyncInfo GetTimeSyncInfo()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -186,9 +216,5 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public ParseErrorTestRestClient() { }
|
public ParseErrorTestRestClient() { }
|
||||||
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(JToken error)
|
|
||||||
{
|
|
||||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public bool Connected { get; set; }
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
public event Action OnClose;
|
public event Action OnClose;
|
||||||
|
|
||||||
|
#pragma warning disable 0067
|
||||||
|
public event Action OnReconnected;
|
||||||
|
public event Action OnReconnecting;
|
||||||
|
#pragma warning restore 0067
|
||||||
public event Action<string> OnMessage;
|
public event Action<string> OnMessage;
|
||||||
public event Action<Exception> OnError;
|
public event Action<Exception> OnError;
|
||||||
public event Action OnOpen;
|
public event Action OnOpen;
|
||||||
|
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
public bool ShouldReconnect { get; set; }
|
public bool ShouldReconnect { get; set; }
|
||||||
@@ -38,6 +44,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public double IncomingKbps => throw new NotImplementedException();
|
public double IncomingKbps => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Uri Uri => new Uri("");
|
||||||
|
|
||||||
|
public TimeSpan KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
public static int lastId = 0;
|
public static int lastId = 0;
|
||||||
public static object lastIdLock = new object();
|
public static object lastIdLock = new object();
|
||||||
|
|
||||||
@@ -89,6 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
Connected = false;
|
Connected = false;
|
||||||
DisconnectTime = DateTime.UtcNow;
|
DisconnectTime = DateTime.UtcNow;
|
||||||
|
Reconnecting = true;
|
||||||
OnClose?.Invoke();
|
OnClose?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,5 +122,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
OnError?.Invoke(error);
|
OnError?.Invoke(error);
|
||||||
}
|
}
|
||||||
|
public Task ReconnectAsync() => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,39 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
||||||
{
|
{
|
||||||
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
SubClient = AddApiClient(new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions));
|
||||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
public TestSocket CreateSocket()
|
||||||
{
|
{
|
||||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||||
return (TestSocket)CreateSocket("123");
|
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestOptions: ClientOptions
|
||||||
|
{
|
||||||
|
public SocketApiClientOptions SubOptions { get; set; } = new SocketApiClientOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestSubSocketClient : SocketApiClient
|
||||||
|
{
|
||||||
|
|
||||||
|
public TestSubSocketClient(ClientOptions options, SocketApiClientOptions apiOptions): base(new Log(""), options, apiOptions)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IWebsocket CreateSocketInternal(string address)
|
||||||
|
{
|
||||||
|
return CreateSocket(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||||
{
|
{
|
||||||
@@ -67,21 +90,4 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestOptions: BaseSocketClientOptions
|
|
||||||
{
|
|
||||||
public ApiClientOptions SubOptions { get; set; } = new ApiClientOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestSubSocketClient : SocketApiClient
|
|
||||||
{
|
|
||||||
|
|
||||||
public TestSubSocketClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
|
||||||
=> new TestAuthProvider(credentials);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,20 +21,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SecureString? Secret { get; }
|
public SecureString? Secret { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The private key to authenticate requests
|
|
||||||
/// </summary>
|
|
||||||
public PrivateKey? PrivateKey { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing a private key for authentication
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="privateKey">The private key used for signing</param>
|
|
||||||
public ApiCredentials(PrivateKey privateKey)
|
|
||||||
{
|
|
||||||
PrivateKey = privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
/// Create Api credentials providing an api key and secret for authentication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -69,11 +55,8 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ApiCredentials Copy()
|
public virtual ApiCredentials Copy()
|
||||||
{
|
{
|
||||||
if (PrivateKey == null)
|
// Use .GetString() to create a copy of the SecureString
|
||||||
// Use .GetString() to create a copy of the SecureString
|
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
|
||||||
else
|
|
||||||
return new ApiCredentials(PrivateKey!.Copy());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -123,7 +106,6 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
{
|
{
|
||||||
Key?.Dispose();
|
Key?.Dispose();
|
||||||
Secret?.Dispose();
|
Secret?.Dispose();
|
||||||
PrivateKey?.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
|
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Security;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Private key info
|
|
||||||
/// </summary>
|
|
||||||
public class PrivateKey : IDisposable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The private key
|
|
||||||
/// </summary>
|
|
||||||
public SecureString Key { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The private key's pass phrase
|
|
||||||
/// </summary>
|
|
||||||
public SecureString? Passphrase { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if the private key is encrypted or not
|
|
||||||
/// </summary>
|
|
||||||
public bool IsEncrypted { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a private key providing an encrypted key information
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The private key used for signing</param>
|
|
||||||
/// <param name="passphrase">The private key's passphrase</param>
|
|
||||||
public PrivateKey(SecureString key, SecureString passphrase)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
Passphrase = passphrase;
|
|
||||||
|
|
||||||
IsEncrypted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a private key providing an encrypted key information
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The private key used for signing</param>
|
|
||||||
/// <param name="passphrase">The private key's passphrase</param>
|
|
||||||
public PrivateKey(string key, string passphrase)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
|
|
||||||
throw new ArgumentException("Key and passphrase can't be null/empty");
|
|
||||||
|
|
||||||
var secureKey = new SecureString();
|
|
||||||
foreach (var c in key)
|
|
||||||
secureKey.AppendChar(c);
|
|
||||||
secureKey.MakeReadOnly();
|
|
||||||
Key = secureKey;
|
|
||||||
|
|
||||||
var securePassphrase = new SecureString();
|
|
||||||
foreach (var c in passphrase)
|
|
||||||
securePassphrase.AppendChar(c);
|
|
||||||
securePassphrase.MakeReadOnly();
|
|
||||||
Passphrase = securePassphrase;
|
|
||||||
|
|
||||||
IsEncrypted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a private key providing an unencrypted key information
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The private key used for signing</param>
|
|
||||||
public PrivateKey(SecureString key)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
|
|
||||||
IsEncrypted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a private key providing an encrypted key information
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The private key used for signing</param>
|
|
||||||
public PrivateKey(string key)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(key))
|
|
||||||
throw new ArgumentException("Key can't be null/empty");
|
|
||||||
|
|
||||||
Key = key.ToSecureString();
|
|
||||||
|
|
||||||
IsEncrypted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copy the private key
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public PrivateKey Copy()
|
|
||||||
{
|
|
||||||
if (Passphrase == null)
|
|
||||||
return new PrivateKey(Key.GetString());
|
|
||||||
else
|
|
||||||
return new PrivateKey(Key.GetString(), Passphrase.GetString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Key?.Dispose();
|
|
||||||
Passphrase?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base API for all API clients
|
/// Base API for all API clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseApiClient: IDisposable
|
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||||
{
|
{
|
||||||
private ApiCredentials? _apiCredentials;
|
private ApiCredentials? _apiCredentials;
|
||||||
private AuthenticationProvider? _authenticationProvider;
|
private AuthenticationProvider? _authenticationProvider;
|
||||||
private bool _created;
|
private bool _created;
|
||||||
private bool _disposing;
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger
|
||||||
|
/// </summary>
|
||||||
|
protected Log _log;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If we are disposing
|
||||||
|
/// </summary>
|
||||||
|
protected bool _disposing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthenticationProvider? AuthenticationProvider
|
public AuthenticationProvider? AuthenticationProvider
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!_created && !_disposing && _apiCredentials != null)
|
if (!_created && !_disposing && _apiCredentials != null)
|
||||||
{
|
{
|
||||||
@@ -70,19 +88,39 @@ namespace CryptoExchange.Net
|
|||||||
internal protected string BaseAddress { get; }
|
internal protected string BaseAddress { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api client options
|
/// Options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal ApiClientOptions Options { get; }
|
public ApiClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
|
/// </summary>
|
||||||
|
protected static int lastId;
|
||||||
|
/// <summary>
|
||||||
|
/// Lock for id generating
|
||||||
|
/// </summary>
|
||||||
|
protected static object idLock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A default serializer
|
||||||
|
/// </summary>
|
||||||
|
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||||
|
Culture = CultureInfo.InvariantCulture
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">Client options</param>
|
/// <param name="log">Logger</param>
|
||||||
|
/// <param name="clientOptions">Client options</param>
|
||||||
/// <param name="apiOptions">Api client options</param>
|
/// <param name="apiOptions">Api client options</param>
|
||||||
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions)
|
protected BaseApiClient(Log log, ClientOptions clientOptions, ApiClientOptions apiOptions)
|
||||||
{
|
{
|
||||||
Options = apiOptions;
|
Options = apiOptions;
|
||||||
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
|
_log = log;
|
||||||
|
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? clientOptions.ApiCredentials?.Copy();
|
||||||
BaseAddress = apiOptions.BaseAddress;
|
BaseAddress = apiOptions.BaseAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,17 +132,223 @@ namespace CryptoExchange.Net
|
|||||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetApiCredentials(ApiCredentials credentials)
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
{
|
{
|
||||||
_apiCredentials = credentials?.Copy();
|
_apiCredentials = credentials?.Copy();
|
||||||
_created = false;
|
_created = false;
|
||||||
_authenticationProvider = null;
|
_authenticationProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data to parse</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<JToken> ValidateJson(string data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(data))
|
||||||
|
{
|
||||||
|
var info = "Empty data object received";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new CallResult<JToken>(JToken.Parse(data));
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a string into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="data">The data to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
|
{
|
||||||
|
var tokenResult = ValidateJson(data);
|
||||||
|
if (!tokenResult)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||||
|
return new CallResult<T>(tokenResult.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a JToken into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="obj">The data to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
|
{
|
||||||
|
serializer ??= _defaultSerializer;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a stream into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="stream">The stream to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||||
|
{
|
||||||
|
serializer ??= _defaultSerializer;
|
||||||
|
string? data = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||||
|
|
||||||
|
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||||
|
// in order to log/return the json data
|
||||||
|
if (Options.OutputOriginalData == true || _log.Level == LogLevel.Trace)
|
||||||
|
{
|
||||||
|
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
_log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||||
|
var result = Deserialize<T>(data, serializer, requestId);
|
||||||
|
if (Options.OutputOriginalData == true)
|
||||||
|
result.OriginalData = data;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||||
|
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||||
|
using var jsonReader = new JsonTextReader(reader);
|
||||||
|
_log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||||
|
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = "[Data only available in Trace LogLevel]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = "[Data only available in Trace LogLevel]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = "[Data only available in Trace LogLevel]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||||
|
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static int NextId()
|
||||||
|
{
|
||||||
|
lock (idLock)
|
||||||
|
{
|
||||||
|
lastId += 1;
|
||||||
|
return lastId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
_apiCredentials?.Dispose();
|
_apiCredentials?.Dispose();
|
||||||
|
|||||||
@@ -2,14 +2,8 @@
|
|||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -30,39 +24,23 @@ namespace CryptoExchange.Net
|
|||||||
/// The log object
|
/// The log object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal Log log;
|
protected internal Log log;
|
||||||
/// <summary>
|
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
|
||||||
/// </summary>
|
|
||||||
protected static int lastId;
|
|
||||||
/// <summary>
|
|
||||||
/// Lock for id generating
|
|
||||||
/// </summary>
|
|
||||||
protected static object idLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A default serializer
|
|
||||||
/// </summary>
|
|
||||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture
|
|
||||||
});
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided client options
|
/// Provided client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BaseClientOptions ClientOptions { get; }
|
public ClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
/// <param name="options">The options for this client</param>
|
/// <param name="options">The options for this client</param>
|
||||||
protected BaseClient(string name, BaseClientOptions options)
|
protected BaseClient(string name, ClientOptions options)
|
||||||
{
|
{
|
||||||
log = new Log(name);
|
log = new Log(name);
|
||||||
log.UpdateWriters(options.LogWriters);
|
log.UpdateWriters(options.LogWriters);
|
||||||
log.Level = options.LogLevel;
|
log.Level = options.LogLevel;
|
||||||
|
options.OnLoggingChanged += HandleLogConfigChange;
|
||||||
|
|
||||||
ClientOptions = options;
|
ClientOptions = options;
|
||||||
|
|
||||||
@@ -71,6 +49,16 @@ namespace CryptoExchange.Net
|
|||||||
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">The credentials to set</param>
|
||||||
|
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register an API client
|
/// Register an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -83,203 +71,12 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
/// Handle a change in the client options log config
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The data to parse</param>
|
private void HandleLogConfigChange()
|
||||||
/// <returns></returns>
|
|
||||||
protected CallResult<JToken> ValidateJson(string data)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(data))
|
log.UpdateWriters(ClientOptions.LogWriters);
|
||||||
{
|
log.Level = ClientOptions.LogLevel;
|
||||||
var info = "Empty data object received";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new CallResult<JToken>(JToken.Parse(data));
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a string into an object
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="data">The data to deserialize</param>
|
|
||||||
/// <param name="serializer">A specific serializer to use</param>
|
|
||||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
|
||||||
{
|
|
||||||
var tokenResult = ValidateJson(data);
|
|
||||||
if (!tokenResult)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
|
||||||
return new CallResult<T>( tokenResult.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a JToken into an object
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="obj">The data to deserialize</param>
|
|
||||||
/// <param name="serializer">A specific serializer to use</param>
|
|
||||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
|
||||||
{
|
|
||||||
serializer ??= defaultSerializer;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<T>(new DeserializeError(info, obj));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<T>(new DeserializeError(info, obj));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<T>(new DeserializeError(info, obj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a stream into an object
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="stream">The stream to deserialize</param>
|
|
||||||
/// <param name="serializer">A specific serializer to use</param>
|
|
||||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
|
||||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
|
||||||
{
|
|
||||||
serializer ??= defaultSerializer;
|
|
||||||
string? data = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
|
||||||
|
|
||||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
|
||||||
// in order to log/return the json data
|
|
||||||
if (ClientOptions.OutputOriginalData || log.Level == LogLevel.Trace)
|
|
||||||
{
|
|
||||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
|
||||||
var result = Deserialize<T>(data, serializer, requestId);
|
|
||||||
if(ClientOptions.OutputOriginalData)
|
|
||||||
result.OriginalData = data;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
|
||||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
|
||||||
using var jsonReader = new JsonTextReader(reader);
|
|
||||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
|
||||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
if (stream.CanSeek)
|
|
||||||
{
|
|
||||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data = "[Data only available in Trace LogLevel]";
|
|
||||||
}
|
|
||||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
|
||||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
if (stream.CanSeek)
|
|
||||||
{
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data = "[Data only available in Trace LogLevel]";
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
|
||||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
if (stream.CanSeek)
|
|
||||||
{
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data = "[Data only available in Trace LogLevel]";
|
|
||||||
}
|
|
||||||
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
|
||||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
|
||||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static int NextId()
|
|
||||||
{
|
|
||||||
lock (idLock)
|
|
||||||
{
|
|
||||||
lastId += 1;
|
|
||||||
return lastId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -288,6 +85,7 @@ namespace CryptoExchange.Net
|
|||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, "Disposing client");
|
log.Write(LogLevel.Debug, "Disposing client");
|
||||||
|
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Requests;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -22,357 +11,18 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The factory for creating requests. Used for unit testing
|
|
||||||
/// </summary>
|
|
||||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request headers to be sent with each request
|
|
||||||
/// </summary>
|
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client options
|
|
||||||
/// </summary>
|
|
||||||
public new BaseRestClientOptions ClientOptions { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
/// <param name="options">The options for this client</param>
|
/// <param name="options">The options for this client</param>
|
||||||
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
|
protected BaseRestClient(string name, ClientOptions options) : base(name, options)
|
||||||
{
|
{
|
||||||
if (options == null)
|
if (options == null)
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
ClientOptions = options;
|
|
||||||
RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetApiCredentials(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="apiClient">The API client the request is for</param>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
|
||||||
RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
bool ignoreRatelimit = false
|
|
||||||
) where T : class
|
|
||||||
{
|
|
||||||
var requestId = NextId();
|
|
||||||
|
|
||||||
if (signed)
|
|
||||||
{
|
|
||||||
var syncTimeResult = await apiClient.SyncTimeAsync().ConfigureAwait(false);
|
|
||||||
if (!syncTimeResult)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
|
||||||
return syncTimeResult.As<T>(default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ignoreRatelimit)
|
|
||||||
{
|
|
||||||
foreach (var limiter in apiClient.RateLimiters)
|
|
||||||
{
|
|
||||||
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult.Success)
|
|
||||||
return new WebCallResult<T>(limitResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signed && apiClient.AuthenticationProvider == null)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
|
||||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
|
||||||
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
|
||||||
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
|
|
||||||
|
|
||||||
string? paramString = "";
|
|
||||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
|
||||||
paramString = $" with request body '{request.Content}'";
|
|
||||||
|
|
||||||
var headers = request.GetHeaders();
|
|
||||||
if (headers.Any())
|
|
||||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
|
||||||
|
|
||||||
apiClient.TotalRequestsMade++;
|
|
||||||
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
|
||||||
return await GetResponseAsync<T>(apiClient, request, deserializer, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The client making the request</param>
|
|
||||||
/// <param name="request">The request object to execute</param>
|
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(BaseApiClient apiClient, IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
sw.Stop();
|
|
||||||
var statusCode = response.StatusCode;
|
|
||||||
var headers = response.ResponseHeaders;
|
|
||||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
|
||||||
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
|
||||||
if (apiClient.manualParseError)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(responseStream);
|
|
||||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}");
|
|
||||||
|
|
||||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
if (!parseResult.Success)
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
|
||||||
|
|
||||||
// Let the library implementation see if it is an error response, and if so parse the error
|
|
||||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
|
||||||
if (error != null)
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
|
||||||
|
|
||||||
// Not an error, so continue deserializing
|
|
||||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data: null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
|
||||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
|
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Http status code indicates error
|
|
||||||
using var reader = new StreamReader(responseStream);
|
|
||||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : parseResult.Error!;
|
|
||||||
if(error.Code == null || error.Code == 0)
|
|
||||||
error.Code = (int)response.StatusCode;
|
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException requestException)
|
|
||||||
{
|
|
||||||
// Request exception, can't reach server for instance
|
|
||||||
var exceptionInfo = requestException.ToLogString();
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
|
||||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException canceledException)
|
|
||||||
{
|
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
|
||||||
{
|
|
||||||
// Cancellation token canceled by caller
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
|
||||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Request timed out
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
|
||||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
|
|
||||||
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
|
|
||||||
/// If the response is an error this method should return the parsed error, else it should return null
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Received data</param>
|
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
|
||||||
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
|
||||||
{
|
|
||||||
return Task.FromResult<ServerError?>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a request object
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The API client the request is for</param>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
|
||||||
/// <param name="requestId">Unique id of a request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IRequest ConstructRequest(
|
|
||||||
RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
Dictionary<string, object>? parameters,
|
|
||||||
bool signed,
|
|
||||||
HttpMethodParameterPosition parameterPosition,
|
|
||||||
ArrayParametersSerialization arraySerialization,
|
|
||||||
int requestId,
|
|
||||||
Dictionary<string, string>? additionalHeaders)
|
|
||||||
{
|
|
||||||
parameters ??= new Dictionary<string, object>();
|
|
||||||
|
|
||||||
for (var i = 0; i< parameters.Count; i++)
|
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> delegateValue)
|
|
||||||
parameters[kvp.Key] = delegateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
|
||||||
{
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
if (apiClient.AuthenticationProvider != null)
|
|
||||||
apiClient.AuthenticationProvider.AuthenticateRequest(
|
|
||||||
apiClient,
|
|
||||||
uri,
|
|
||||||
method,
|
|
||||||
parameters,
|
|
||||||
signed,
|
|
||||||
arraySerialization,
|
|
||||||
parameterPosition,
|
|
||||||
out uriParameters,
|
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
foreach(var param in parameters)
|
|
||||||
{
|
|
||||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
|
||||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
|
||||||
$"should return provided parameters in either the uri or body parameters output");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
|
||||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(method, uri, requestId);
|
|
||||||
request.Accept = Constants.JsonContentHeader;
|
|
||||||
|
|
||||||
foreach (var header in headers)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
|
|
||||||
if (additionalHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (var header in additionalHeaders)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StandardRequestHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (var header in StandardRequestHeaders)
|
|
||||||
// Only add it if it isn't overwritten
|
|
||||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
|
||||||
{
|
|
||||||
var contentType = apiClient.requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
|
||||||
if (bodyParameters.Any())
|
|
||||||
WriteParamBody(apiClient, request, bodyParameters, contentType);
|
|
||||||
else
|
|
||||||
request.SetContent(apiClient.requestBodyEmptyContent, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the parameters of the request to the request object body
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The client making the request</param>
|
|
||||||
/// <param name="request">The request to set the parameters on</param>
|
|
||||||
/// <param name="parameters">The parameters to set</param>
|
|
||||||
/// <param name="contentType">The content type of the data</param>
|
|
||||||
protected virtual void WriteParamBody(BaseApiClient apiClient, IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
|
||||||
{
|
|
||||||
if (apiClient.requestBodyFormat == RequestBodyFormat.Json)
|
|
||||||
{
|
|
||||||
// Write the parameters as json in the body
|
|
||||||
var stringData = JsonConvert.SerializeObject(parameters);
|
|
||||||
request.SetContent(stringData, contentType);
|
|
||||||
}
|
|
||||||
else if (apiClient.requestBodyFormat == RequestBodyFormat.FormData)
|
|
||||||
{
|
|
||||||
// Write the parameters as form data in the body
|
|
||||||
var stringData = parameters.ToFormData();
|
|
||||||
request.SetContent(stringData, contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="error">The string the request returned</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Error ParseErrorResponse(JToken error)
|
|
||||||
{
|
|
||||||
return new ServerError(error.ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -22,81 +17,18 @@ namespace CryptoExchange.Net
|
|||||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||||
{
|
{
|
||||||
#region fields
|
#region fields
|
||||||
/// <summary>
|
|
||||||
/// The factory for creating sockets. Used for unit testing
|
|
||||||
/// </summary>
|
|
||||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of socket connections currently connecting/connected
|
|
||||||
/// </summary>
|
|
||||||
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new();
|
|
||||||
/// <summary>
|
|
||||||
/// Semaphore used while creating sockets
|
|
||||||
/// </summary>
|
|
||||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of concurrent socket connections
|
|
||||||
/// </summary>
|
|
||||||
protected int MaxSocketConnections { get; set; } = 9999;
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
|
||||||
/// </summary>
|
|
||||||
protected Func<byte[], string>? dataInterpreterBytes;
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
|
||||||
/// </summary>
|
|
||||||
protected Func<string, string>? dataInterpreterString;
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
|
||||||
/// </summary>
|
|
||||||
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
|
|
||||||
/// <summary>
|
|
||||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
|
|
||||||
/// </summary>
|
|
||||||
protected Task? periodicTask;
|
|
||||||
/// <summary>
|
|
||||||
/// Wait event for the periodicTask
|
|
||||||
/// </summary>
|
|
||||||
protected AsyncResetEvent? periodicEvent;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool disposing;
|
protected bool disposing;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true; data which is a response to a query will also be distributed to subscriptions
|
|
||||||
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
|
|
||||||
/// </summary>
|
|
||||||
protected internal bool ContinueOnQueryResponse { get; protected set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
|
||||||
/// </summary>
|
|
||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of outgoing messages per socket per second
|
|
||||||
/// </summary>
|
|
||||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||||
{
|
/// <inheritdoc />
|
||||||
get
|
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||||
{
|
/// <inheritdoc />
|
||||||
if (!sockets.Any())
|
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||||
return 0;
|
|
||||||
|
|
||||||
return sockets.Sum(s => s.Value.Socket.IncomingKbps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client options
|
|
||||||
/// </summary>
|
|
||||||
public new BaseSocketClientOptions ClientOptions { get; }
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -104,532 +36,8 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
/// <param name="options">The options for this client</param>
|
/// <param name="options">The options for this client</param>
|
||||||
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
protected BaseSocketClient(string name, ClientOptions options) : base(name, options)
|
||||||
{
|
{
|
||||||
if (options == null)
|
|
||||||
throw new ArgumentNullException(nameof(options));
|
|
||||||
|
|
||||||
ClientOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetApiCredentials(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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>
|
|
||||||
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
|
|
||||||
{
|
|
||||||
dataInterpreterBytes = byteHandler;
|
|
||||||
dataInterpreterString = stringHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect to an url and listen for data on the BaseAddress
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type of the expected data</typeparam>
|
|
||||||
/// <param name="apiClient">The API client the subscription is for</param>
|
|
||||||
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
|
||||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
|
||||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
|
||||||
/// <param name="dataHandler">The handler of update data</param>
|
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
|
||||||
{
|
|
||||||
return SubscribeAsync(apiClient, apiClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect to an url and listen for data
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type of the expected data</typeparam>
|
|
||||||
/// <param name="apiClient">The API client the subscription is for</param>
|
|
||||||
/// <param name="url">The URL to connect to</param>
|
|
||||||
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
|
||||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
|
||||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
|
||||||
/// <param name="dataHandler">The handler of update data</param>
|
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
|
||||||
SocketSubscription subscription;
|
|
||||||
var released = false;
|
|
||||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
|
||||||
// This is necessary for being able to see if connections can be combined
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get a new or existing socket connection
|
|
||||||
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
|
||||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
|
|
||||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
|
||||||
{
|
|
||||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
released = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var needsConnecting = !socketConnection.Connected;
|
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if(!released)
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.Socket.Id} has been paused, can't subscribe at this moment");
|
|
||||||
return new CallResult<UpdateSubscription>( new ServerError("Socket is paused"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request != null)
|
|
||||||
{
|
|
||||||
// Send the request and wait for answer
|
|
||||||
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
|
||||||
if (!subResult)
|
|
||||||
{
|
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No request to be sent, so just mark the subscription as comfirmed
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
socketConnection.ShouldReconnect = true;
|
|
||||||
if (ct != default)
|
|
||||||
{
|
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} Cancellation token set, closing subscription");
|
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} subscription completed");
|
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the subscribe request and waits for a response to that request
|
|
||||||
/// </summary>
|
|
||||||
/// <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>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
|
||||||
{
|
|
||||||
CallResult<object>? callResult = null;
|
|
||||||
await socketConnection.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (callResult?.Success == true)
|
|
||||||
{
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
return new CallResult<bool>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(callResult== null)
|
|
||||||
return new CallResult<bool>(new ServerError("No response on subscription request received"));
|
|
||||||
|
|
||||||
return new CallResult<bool>(callResult.Error!);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Expected result type</typeparam>
|
|
||||||
/// <param name="apiClient">The API client the query is for</param>
|
|
||||||
/// <param name="request">The request to send, will be serialized to json</param>
|
|
||||||
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, object request, bool authenticated)
|
|
||||||
{
|
|
||||||
return QueryAsync<T>(apiClient, apiClient.Options.BaseAddress, request, authenticated);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a query on a socket connection and wait for the response
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The expected result type</typeparam>
|
|
||||||
/// <param name="apiClient">The API client the query is for</param>
|
|
||||||
/// <param name="url">The url for the request</param>
|
|
||||||
/// <param name="request">The request to send</param>
|
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, string url, object request, bool authenticated)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
|
||||||
var released = false;
|
|
||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
|
||||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
|
||||||
{
|
|
||||||
// Can release early when only a single sub per connection
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
released = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult<T>(connectResult.Error!);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
//When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
|
|
||||||
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
|
|
||||||
if (!released)
|
|
||||||
semaphoreSlim.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.Socket.Id} has been paused, can't send query at this moment");
|
|
||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the query request and waits for the result
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The expected result type</typeparam>
|
|
||||||
/// <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>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
|
||||||
{
|
|
||||||
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
|
||||||
await socket.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data =>
|
|
||||||
{
|
|
||||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
dataResult = callResult;
|
|
||||||
return true;
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return dataResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
|
||||||
{
|
|
||||||
if (socket.Connected)
|
|
||||||
return new CallResult<bool>(true);
|
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult<bool>(connectResult.Error!);
|
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
|
||||||
return new CallResult<bool>(true);
|
|
||||||
|
|
||||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} authentication failed");
|
|
||||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
|
||||||
return new CallResult<bool>(result.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.Authenticated = true;
|
|
||||||
return new CallResult<bool>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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 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 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 socketConnection, object request, JToken data, [NotNullWhen(true)]out CallResult<T>? callResult);
|
|
||||||
/// <summary>
|
|
||||||
/// 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="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 socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
|
|
||||||
/// <summary>
|
|
||||||
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
|
|
||||||
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
|
||||||
/// <param name="message">The received data</param>
|
|
||||||
/// <param name="request">The subscription request</param>
|
|
||||||
/// <returns>True if the message is for the subscription which sent the request</returns>
|
|
||||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
|
|
||||||
/// <summary>
|
|
||||||
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
|
|
||||||
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
|
||||||
/// <param name="message">The received data</param>
|
|
||||||
/// <param name="identifier">The string identifier of the handler</param>
|
|
||||||
/// <returns>True if the message is for the handler which has the identifier</returns>
|
|
||||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
|
|
||||||
/// <summary>
|
|
||||||
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socketConnection">The socket connection that should be authenticated</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
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="subscriptionToUnsub">The subscription to unsubscribe</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional handler to interpolate data before sending it to the handlers
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal virtual JToken ProcessTokenData(JToken message)
|
|
||||||
{
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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>
|
|
||||||
/// <param name="identifier">The identifier of the subscription (can be null if request param is used)</param>
|
|
||||||
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
|
|
||||||
/// <param name="connection">The socket connection the handler is on</param>
|
|
||||||
/// <param name="dataHandler">The handler of the data received</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual SocketSubscription AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler)
|
|
||||||
{
|
|
||||||
void InternalHandler(MessageEvent messageEvent)
|
|
||||||
{
|
|
||||||
if (typeof(T) == typeof(string))
|
|
||||||
{
|
|
||||||
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
|
||||||
dataHandler(new DataEvent<T>(stringData, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
|
||||||
if (!desResult)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {connection.Socket.Id} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataHandler(new DataEvent<T>(desResult.Data, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = request == null
|
|
||||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, InternalHandler)
|
|
||||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, InternalHandler);
|
|
||||||
connection.AddSubscription(subscription);
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a generic message handler. Used for example to reply to ping requests
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
|
|
||||||
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
|
|
||||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
|
||||||
{
|
|
||||||
genericHandlers.Add(identifier, action);
|
|
||||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action);
|
|
||||||
foreach (var connection in sockets.Values)
|
|
||||||
connection.AddSubscription(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The API client the connection is for</param>
|
|
||||||
/// <param name="address">The address the socket is for</param>
|
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual SocketConnection GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
|
||||||
{
|
|
||||||
var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/')
|
|
||||||
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
|
||||||
{
|
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new socket
|
|
||||||
var socket = CreateSocket(address);
|
|
||||||
var socketConnection = new SocketConnection(this, apiClient, socket);
|
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
|
||||||
foreach (var kvp in genericHandlers)
|
|
||||||
{
|
|
||||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, kvp.Value);
|
|
||||||
socketConnection.AddSubscription(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
return socketConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process an unhandled message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="token">The token that wasn't processed</param>
|
|
||||||
protected virtual void HandleUnhandledMessage(JToken token)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect a socket
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="socketConnection">The socket to connect</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
|
||||||
{
|
|
||||||
if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
sockets.TryAdd(socketConnection.Socket.Id, socketConnection);
|
|
||||||
return new CallResult<bool>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
socketConnection.Socket.Dispose();
|
|
||||||
return new CallResult<bool>(new CantConnectError());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a socket for an address
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">The address the socket should connect to</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IWebsocket CreateSocket(string address)
|
|
||||||
{
|
|
||||||
var socket = SocketFactory.CreateWebsocket(log, address);
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
|
||||||
|
|
||||||
if (ClientOptions.Proxy != null)
|
|
||||||
socket.SetProxy(ClientOptions.Proxy);
|
|
||||||
|
|
||||||
socket.Timeout = ClientOptions.SocketNoDataTimeout;
|
|
||||||
socket.DataInterpreterBytes = dataInterpreterBytes;
|
|
||||||
socket.DataInterpreterString = dataInterpreterString;
|
|
||||||
socket.RatelimitPerSecond = RateLimitPerSocketPerSecond;
|
|
||||||
socket.OnError += e =>
|
|
||||||
{
|
|
||||||
if(e is WebSocketException wse)
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
|
|
||||||
else
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: " + e.ToLogString());
|
|
||||||
};
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Periodically sends data over a socket connection
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="identifier">Identifier for the periodic send</param>
|
|
||||||
/// <param name="interval">How often</param>
|
|
||||||
/// <param name="objGetter">Method returning the object to send</param>
|
|
||||||
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
|
||||||
{
|
|
||||||
if (objGetter == null)
|
|
||||||
throw new ArgumentNullException(nameof(objGetter));
|
|
||||||
|
|
||||||
periodicEvent = new AsyncResetEvent();
|
|
||||||
periodicTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
while (!disposing)
|
|
||||||
{
|
|
||||||
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
|
|
||||||
if (disposing)
|
|
||||||
break;
|
|
||||||
|
|
||||||
foreach (var socket in sockets.Values)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (!socket.Socket.IsOpen)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var obj = objGetter(socket);
|
|
||||||
if (obj == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic {identifier}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
socket.Send(obj);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} Periodic send {identifier} failed: " + ex.ToLogString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -639,24 +47,12 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||||
{
|
{
|
||||||
|
foreach(var socket in ApiClients.OfType<SocketApiClient>())
|
||||||
SocketSubscription? subscription = null;
|
|
||||||
SocketConnection? connection = null;
|
|
||||||
foreach(var socket in sockets.Values.ToList())
|
|
||||||
{
|
{
|
||||||
subscription = socket.GetSubscription(subscriptionId);
|
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||||
if (subscription != null)
|
if (result)
|
||||||
{
|
break;
|
||||||
connection = socket;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscription == null || connection == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
log.Write(LogLevel.Information, "Closing subscription " + subscriptionId);
|
|
||||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -669,7 +65,7 @@ namespace CryptoExchange.Net
|
|||||||
if (subscription == null)
|
if (subscription == null)
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
log.Write(LogLevel.Information, "Closing subscription " + subscription.Id);
|
log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,33 +75,37 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Information, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
var tasks = new List<Task>();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
await Task.Run(async () =>
|
tasks.Add(client.UnsubscribeAllAsync());
|
||||||
{
|
|
||||||
var tasks = new List<Task>();
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
{
|
|
||||||
var socketList = sockets.Values;
|
|
||||||
foreach (var sub in socketList)
|
|
||||||
tasks.Add(sub.CloseAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose the client
|
/// Reconnect all connections
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override void Dispose()
|
/// <returns></returns>
|
||||||
|
public virtual async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
disposing = true;
|
log.Write(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||||
periodicEvent?.Set();
|
var tasks = new List<Task>();
|
||||||
periodicEvent?.Dispose();
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
{
|
||||||
Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult();
|
tasks.Add(client.ReconnectAsync());
|
||||||
semaphoreSlim?.Dispose();
|
}
|
||||||
base.Dispose();
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
public string GetSubscriptionsState()
|
||||||
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
foreach(var client in ApiClients.OfType<SocketApiClient>())
|
||||||
|
result.AppendLine(client.GetSubscriptionsState());
|
||||||
|
return result.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,42 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base rest API client for interacting with a REST API
|
/// Base rest API client for interacting with a REST API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class RestApiClient: BaseApiClient
|
public abstract class RestApiClient : BaseApiClient, IRestApiClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Get time sync info for an API client
|
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||||
/// </summary>
|
/// <inheritdoc />
|
||||||
/// <returns></returns>
|
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
||||||
protected abstract TimeSyncInfo GetTimeSyncInfo();
|
|
||||||
|
/// <inheritdoc />
|
||||||
/// <summary>
|
public abstract TimeSpan? GetTimeOffset();
|
||||||
/// Get time offset for an API client
|
|
||||||
/// </summary>
|
/// <inheritdoc />
|
||||||
/// <returns></returns>
|
public int TotalRequestsMade { get; set; }
|
||||||
public abstract TimeSpan GetTimeOffset();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total amount of requests made with this API client
|
/// Request headers to be sent with each request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalRequestsMade { get; set; }
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for this client
|
/// Options for this client
|
||||||
@@ -41,28 +48,502 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options
|
||||||
|
/// </summary>
|
||||||
|
internal ClientOptions ClientOptions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="log">Logger</param>
|
||||||
/// <param name="options">The base client options</param>
|
/// <param name="options">The base client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
|
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions) : base(log, options, apiOptions)
|
||||||
{
|
{
|
||||||
var rateLimiters = new List<IRateLimiter>();
|
var rateLimiters = new List<IRateLimiter>();
|
||||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||||
rateLimiters.Add(rateLimiter);
|
rateLimiters.Add(rateLimiter);
|
||||||
RateLimiters = rateLimiters;
|
RateLimiters = rateLimiters;
|
||||||
|
ClientOptions = options;
|
||||||
|
|
||||||
|
RequestFactory.Configure(apiOptions.RequestTimeout, options.Proxy, apiOptions.HttpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a request to the uri and returns if it was successful
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNull]
|
||||||
|
protected virtual async Task<WebCallResult> SendRequestAsync(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false)
|
||||||
|
{
|
||||||
|
int currentTry = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
currentTry++;
|
||||||
|
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult(request.Error!);
|
||||||
|
|
||||||
|
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||||
|
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return result.AsDataless();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNull]
|
||||||
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false
|
||||||
|
) where T : class
|
||||||
|
{
|
||||||
|
int currentTry = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
currentTry++;
|
||||||
|
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult<T>(request.Error!);
|
||||||
|
|
||||||
|
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||||
|
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares a request to be sent to the server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false)
|
||||||
|
{
|
||||||
|
var requestId = NextId();
|
||||||
|
|
||||||
|
if (signed)
|
||||||
|
{
|
||||||
|
var syncTask = SyncTimeAsync();
|
||||||
|
var timeSyncInfo = GetTimeSyncInfo();
|
||||||
|
|
||||||
|
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||||
|
{
|
||||||
|
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||||
|
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||||
|
if (!syncTimeResult)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||||
|
return syncTimeResult.As<IRequest>(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreRatelimit)
|
||||||
|
{
|
||||||
|
foreach (var limiter in RateLimiters)
|
||||||
|
{
|
||||||
|
var limitResult = await limiter.LimitRequestAsync(_log, uri.AbsolutePath, method, signed, Options.ApiCredentials?.Key, Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!limitResult.Success)
|
||||||
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signed && AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||||
|
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||||
|
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||||
|
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||||
|
|
||||||
|
string? paramString = "";
|
||||||
|
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||||
|
paramString = $" with request body '{request.Content}'";
|
||||||
|
|
||||||
|
var headers = request.GetHeaders();
|
||||||
|
if (headers.Any())
|
||||||
|
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||||
|
|
||||||
|
TotalRequestsMade++;
|
||||||
|
_log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||||
|
return new CallResult<IRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request object to execute</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||||
|
IRequest request,
|
||||||
|
JsonSerializer? deserializer,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
bool expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
sw.Stop();
|
||||||
|
var statusCode = response.StatusCode;
|
||||||
|
var headers = response.ResponseHeaders;
|
||||||
|
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
||||||
|
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
||||||
|
if (manualParseError)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(responseStream);
|
||||||
|
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
_log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||||
|
|
||||||
|
if (!expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
|
// Let the library implementation see if it is an error response, and if so parse the error
|
||||||
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
|
if (error != null)
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
|
||||||
|
// Not an error, so continue deserializing
|
||||||
|
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data))
|
||||||
|
{
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
// Not empty, and not json
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
|
if (error != null)
|
||||||
|
// Error response
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty success response; okay
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||||
|
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, Options.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Http status code indicates error
|
||||||
|
using var reader = new StreamReader(responseStream);
|
||||||
|
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||||
|
if (error.Code == null || error.Code == 0)
|
||||||
|
error.Code = (int)response.StatusCode;
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException requestException)
|
||||||
|
{
|
||||||
|
// Request exception, can't reach server for instance
|
||||||
|
var exceptionInfo = requestException.ToLogString();
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException canceledException)
|
||||||
|
{
|
||||||
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
|
{
|
||||||
|
// Cancellation token canceled by caller
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Request timed out
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
|
||||||
|
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
|
||||||
|
/// If the response is an error this method should return the parsed error, else it should return null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Received data</param>
|
||||||
|
/// <returns>Null if not an error, Error otherwise</returns>
|
||||||
|
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
||||||
|
{
|
||||||
|
return Task.FromResult<ServerError?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||||
|
/// Note that this is always called; even when the request might be successful
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
||||||
|
/// <param name="callResult">The result of the call</param>
|
||||||
|
/// <param name="tries">The current try number</param>
|
||||||
|
/// <returns>True if call should retry, false if the call should return</returns>
|
||||||
|
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a request object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
||||||
|
/// <param name="requestId">Unique id of a request</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual IRequest ConstructRequest(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
Dictionary<string, object>? parameters,
|
||||||
|
bool signed,
|
||||||
|
HttpMethodParameterPosition parameterPosition,
|
||||||
|
ArrayParametersSerialization arraySerialization,
|
||||||
|
int requestId,
|
||||||
|
Dictionary<string, string>? additionalHeaders)
|
||||||
|
{
|
||||||
|
parameters ??= new Dictionary<string, object>();
|
||||||
|
|
||||||
|
for (var i = 0; i < parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var kvp = parameters.ElementAt(i);
|
||||||
|
if (kvp.Value is Func<object> delegateValue)
|
||||||
|
parameters[kvp.Key] = delegateValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||||
|
{
|
||||||
|
foreach (var parameter in parameters)
|
||||||
|
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||||
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||||
|
if (AuthenticationProvider != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AuthenticationProvider.AuthenticateRequest(
|
||||||
|
this,
|
||||||
|
uri,
|
||||||
|
method,
|
||||||
|
parameters,
|
||||||
|
signed,
|
||||||
|
arraySerialization,
|
||||||
|
parameterPosition,
|
||||||
|
out uriParameters,
|
||||||
|
out bodyParameters,
|
||||||
|
out headers);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
foreach (var param in parameters)
|
||||||
|
{
|
||||||
|
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||||
|
{
|
||||||
|
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||||
|
$"should return provided parameters in either the uri or body parameters output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
|
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||||
|
|
||||||
|
var request = RequestFactory.Create(method, uri, requestId);
|
||||||
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
|
foreach (var header in headers)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
|
||||||
|
if (additionalHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in additionalHeaders)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StandardRequestHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in StandardRequestHeaders)
|
||||||
|
{
|
||||||
|
// Only add it if it isn't overwritten
|
||||||
|
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
|
{
|
||||||
|
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
|
if (bodyParameters.Any())
|
||||||
|
WriteParamBody(request, bodyParameters, contentType);
|
||||||
|
else
|
||||||
|
request.SetContent(requestBodyEmptyContent, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the parameters of the request to the request object body
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request to set the parameters on</param>
|
||||||
|
/// <param name="parameters">The parameters to set</param>
|
||||||
|
/// <param name="contentType">The content type of the data</param>
|
||||||
|
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||||
|
{
|
||||||
|
if (requestBodyFormat == RequestBodyFormat.Json)
|
||||||
|
{
|
||||||
|
// Write the parameters as json in the body
|
||||||
|
var stringData = JsonConvert.SerializeObject(parameters);
|
||||||
|
request.SetContent(stringData, contentType);
|
||||||
|
}
|
||||||
|
else if (requestBodyFormat == RequestBodyFormat.FormData)
|
||||||
|
{
|
||||||
|
// Write the parameters as form data in the body
|
||||||
|
var stringData = parameters.ToFormData();
|
||||||
|
request.SetContent(stringData, contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The string the request returned</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Error ParseErrorResponse(JToken error)
|
||||||
|
{
|
||||||
|
return new ServerError(error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Server time</returns>
|
/// <returns>Server time</returns>
|
||||||
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
|
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
|
|
||||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
|
if (timeSyncParams == null)
|
||||||
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||||
@@ -92,9 +573,9 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time offset between local and server
|
// Calculate time offset between local and server
|
||||||
var offset = result.Data - localTime;
|
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
|
||||||
timeSyncParams.UpdateTimeOffset(offset);
|
timeSyncParams.UpdateTimeOffset(offset);
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||||
|
|||||||
@@ -1,19 +1,808 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base socket API client for interaction with a websocket API
|
/// Base socket API client for interaction with a websocket API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SocketApiClient : BaseApiClient
|
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
||||||
{
|
{
|
||||||
|
#region Fields
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of socket connections currently connecting/connected
|
||||||
|
/// </summary>
|
||||||
|
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Semaphore used while creating sockets
|
||||||
|
/// </summary>
|
||||||
|
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||||
|
/// <summary>
|
||||||
|
/// Keep alive interval for websocket connection
|
||||||
|
/// </summary>
|
||||||
|
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
protected Func<byte[], string>? dataInterpreterBytes;
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
protected Func<string, string>? dataInterpreterString;
|
||||||
|
/// <summary>
|
||||||
|
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||||
|
/// </summary>
|
||||||
|
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
|
||||||
|
/// <summary>
|
||||||
|
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
|
||||||
|
/// </summary>
|
||||||
|
protected Task? periodicTask;
|
||||||
|
/// <summary>
|
||||||
|
/// Wait event for the periodicTask
|
||||||
|
/// </summary>
|
||||||
|
protected AsyncResetEvent? periodicEvent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true; data which is a response to a query will also be distributed to subscriptions
|
||||||
|
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool ContinueOnQueryResponse { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of outgoing messages per socket per second
|
||||||
|
/// </summary>
|
||||||
|
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public double IncomingKbps
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!socketConnections.Any())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentConnections => socketConnections.Count;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentSubscriptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!socketConnections.Any())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public new SocketApiClientOptions Options => (SocketApiClientOptions)base.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options
|
||||||
|
/// </summary>
|
||||||
|
internal ClientOptions ClientOptions { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">The base client options</param>
|
/// <param name="log">log</param>
|
||||||
|
/// <param name="options">Client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions) : base(log, options, apiOptions)
|
||||||
{
|
{
|
||||||
|
ClientOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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>
|
||||||
|
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
|
||||||
|
{
|
||||||
|
dataInterpreterBytes = byteHandler;
|
||||||
|
dataInterpreterString = stringHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect to an url and listen for data on the BaseAddress
|
||||||
|
/// </summary>
|
||||||
|
/// <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>
|
||||||
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return SubscribeAsync(Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect to an url and listen for data
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||||
|
/// <param name="url">The URL to connect to</param>
|
||||||
|
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||||
|
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||||
|
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||||
|
/// <param name="dataHandler">The handler of update data</param>
|
||||||
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_disposing)
|
||||||
|
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
|
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
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Get a new or existing socket connection
|
||||||
|
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
|
// Add a subscription on the socket connection
|
||||||
|
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Options.SocketSubscriptionsCombineTarget == 1)
|
||||||
|
{
|
||||||
|
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
released = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsConnecting = !socketConnection.Connected;
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!released)
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketConnection.PausedActivity)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
|
||||||
|
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request != null)
|
||||||
|
{
|
||||||
|
// Send the request and wait for answer
|
||||||
|
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
||||||
|
if (!subResult)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
|
||||||
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No request to be sent, so just mark the subscription as comfirmed
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct != default)
|
||||||
|
{
|
||||||
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||||
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
|
||||||
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the subscribe request and waits for a response to that request
|
||||||
|
/// </summary>
|
||||||
|
/// <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>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||||
|
{
|
||||||
|
CallResult<object>? callResult = null;
|
||||||
|
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (callResult?.Success == true)
|
||||||
|
{
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callResult == null)
|
||||||
|
return new CallResult<bool>(new ServerError("No response on subscription request received"));
|
||||||
|
|
||||||
|
return new CallResult<bool>(callResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Expected result type</typeparam>
|
||||||
|
/// <param name="request">The request to send, will be serialized to json</param>
|
||||||
|
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
|
||||||
|
{
|
||||||
|
return QueryAsync<T>(Options.BaseAddress, request, authenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a query on a socket connection and wait for the response
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected result type</typeparam>
|
||||||
|
/// <param name="url">The url for the request</param>
|
||||||
|
/// <param name="request">The request to send</param>
|
||||||
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
|
||||||
|
{
|
||||||
|
if (_disposing)
|
||||||
|
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
|
|
||||||
|
SocketConnection socketConnection;
|
||||||
|
var released = false;
|
||||||
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.As<T>(default);
|
||||||
|
|
||||||
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
|
if (Options.SocketSubscriptionsCombineTarget == 1)
|
||||||
|
{
|
||||||
|
// Can release early when only a single sub per connection
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
released = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult<T>(connectResult.Error!);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!released)
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketConnection.PausedActivity)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
|
||||||
|
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the query request and waits for the result
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected result type</typeparam>
|
||||||
|
/// <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>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
||||||
|
{
|
||||||
|
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||||
|
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, null, data =>
|
||||||
|
{
|
||||||
|
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
dataResult = callResult;
|
||||||
|
return true;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return dataResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||||
|
{
|
||||||
|
if (socket.Connected)
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
|
||||||
|
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult<bool>(connectResult.Error!);
|
||||||
|
|
||||||
|
if (Options.DelayAfterConnect != TimeSpan.Zero)
|
||||||
|
await Task.Delay(Options.DelayAfterConnect).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!authenticated || socket.Authenticated)
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||||
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||||
|
if (socket.Connected)
|
||||||
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||||
|
return new CallResult<bool>(result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Authenticated = true;
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 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 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 socketConnection, object request, JToken data, [NotNullWhen(true)] out CallResult<T>? callResult);
|
||||||
|
/// <summary>
|
||||||
|
/// 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="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 socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
|
||||||
|
/// <summary>
|
||||||
|
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
|
||||||
|
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
||||||
|
/// <param name="message">The received data</param>
|
||||||
|
/// <param name="request">The subscription request</param>
|
||||||
|
/// <returns>True if the message is for the subscription which sent the request</returns>
|
||||||
|
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
|
||||||
|
/// <summary>
|
||||||
|
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
|
||||||
|
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
||||||
|
/// <param name="message">The received data</param>
|
||||||
|
/// <param name="identifier">The string identifier of the handler</param>
|
||||||
|
/// <returns>True if the message is for the handler which has the identifier</returns>
|
||||||
|
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
|
||||||
|
/// <summary>
|
||||||
|
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The socket connection that should be authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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="subscriptionToUnsub">The subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional handler to interpolate data before sending it to the handlers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal virtual JToken ProcessTokenData(JToken message)
|
||||||
|
{
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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>
|
||||||
|
/// <param name="identifier">The identifier of the subscription (can be null if request param is used)</param>
|
||||||
|
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
|
||||||
|
/// <param name="connection">The socket connection the handler is on</param>
|
||||||
|
/// <param name="dataHandler">The handler of the data received</param>
|
||||||
|
/// <param name="authenticated">Whether the subscription needs authentication</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
|
||||||
|
{
|
||||||
|
void InternalHandler(MessageEvent messageEvent)
|
||||||
|
{
|
||||||
|
if (typeof(T) == typeof(string))
|
||||||
|
{
|
||||||
|
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
||||||
|
dataHandler(new DataEvent<T>(stringData, null, Options.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||||
|
if (!desResult)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataHandler(new DataEvent<T>(desResult.Data, null, Options.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = request == null
|
||||||
|
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||||
|
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||||
|
if (!connection.AddSubscription(subscription))
|
||||||
|
return null;
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a generic message handler. Used for example to reply to ping requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
|
||||||
|
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
|
||||||
|
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
||||||
|
{
|
||||||
|
genericHandlers.Add(identifier, action);
|
||||||
|
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
|
||||||
|
foreach (var connection in socketConnections.Values)
|
||||||
|
connection.AddSubscription(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address"></param>
|
||||||
|
/// <param name="authentication"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new CallResult<string?>(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the url to reconnect to after losing a connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||||
|
{
|
||||||
|
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The original request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new CallResult<object>(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address the socket is for</param>
|
||||||
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
||||||
|
{
|
||||||
|
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
|
&& (s.Value.ApiClient.GetType() == GetType())
|
||||||
|
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
||||||
|
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
if (result.SubscriptionCount < Options.SocketSubscriptionsCombineTarget || (socketConnections.Count >= Options.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= Options.SocketSubscriptionsCombineTarget)))
|
||||||
|
{
|
||||||
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
|
return new CallResult<SocketConnection>(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectionAddress)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||||
|
return connectionAddress.As<SocketConnection>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionAddress.Data != address)
|
||||||
|
_log.Write(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||||
|
|
||||||
|
// Create new socket
|
||||||
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
|
var socketConnection = new SocketConnection(_log, this, socket, address);
|
||||||
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
foreach (var kvp in genericHandlers)
|
||||||
|
{
|
||||||
|
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
|
||||||
|
socketConnection.AddSubscription(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult<SocketConnection>(socketConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process an unhandled message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The token that wasn't processed</param>
|
||||||
|
protected virtual void HandleUnhandledMessage(JToken token)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect a socket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socketConnection">The socket to connect</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
||||||
|
{
|
||||||
|
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
socketConnection.Dispose();
|
||||||
|
return new CallResult<bool>(new CantConnectError());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get parameters for the websocket connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address to connect to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||||
|
=> new(new Uri(address), Options.AutoReconnect)
|
||||||
|
{
|
||||||
|
DataInterpreterBytes = dataInterpreterBytes,
|
||||||
|
DataInterpreterString = dataInterpreterString,
|
||||||
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
|
ReconnectInterval = Options.ReconnectInterval,
|
||||||
|
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||||
|
Proxy = ClientOptions.Proxy,
|
||||||
|
Timeout = Options.SocketNoDataTimeout
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a socket for an address
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address the socket should connect to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual IWebsocket CreateSocket(string address)
|
||||||
|
{
|
||||||
|
var socket = SocketFactory.CreateWebsocket(_log, GetWebSocketParameters(address));
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Periodically sends data over a socket connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">Identifier for the periodic send</param>
|
||||||
|
/// <param name="interval">How often</param>
|
||||||
|
/// <param name="objGetter">Method returning the object to send</param>
|
||||||
|
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||||
|
{
|
||||||
|
if (objGetter == null)
|
||||||
|
throw new ArgumentNullException(nameof(objGetter));
|
||||||
|
|
||||||
|
periodicEvent = new AsyncResetEvent();
|
||||||
|
periodicTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!_disposing)
|
||||||
|
{
|
||||||
|
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
|
||||||
|
if (_disposing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (var socketConnection in socketConnections.Values)
|
||||||
|
{
|
||||||
|
if (_disposing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!socketConnection.Connected)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var obj = objGetter(socketConnection);
|
||||||
|
if (obj == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socketConnection.Send(obj);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task<bool> UnsubscribeAsync(int subscriptionId)
|
||||||
|
{
|
||||||
|
SocketSubscription? subscription = null;
|
||||||
|
SocketConnection? connection = null;
|
||||||
|
foreach (var socket in socketConnections.Values.ToList())
|
||||||
|
{
|
||||||
|
subscription = socket.GetSubscription(subscriptionId);
|
||||||
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
connection = socket;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription == null || connection == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
|
||||||
|
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
||||||
|
{
|
||||||
|
if (subscription == null)
|
||||||
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||||
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe all subscriptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
{
|
||||||
|
var socketList = socketConnections.Values;
|
||||||
|
foreach (var sub in socketList)
|
||||||
|
tasks.Add(sub.CloseAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect all connections
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task ReconnectAsync()
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
{
|
||||||
|
var socketList = socketConnections.Values;
|
||||||
|
foreach (var sub in socketList)
|
||||||
|
tasks.Add(sub.TriggerReconnectAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
public string GetSubscriptionsState()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
|
||||||
|
foreach (var connection in socketConnections)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
|
||||||
|
foreach (var subscription in connection.Value.Subscriptions)
|
||||||
|
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose the client
|
||||||
|
/// </summary>
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_disposing = true;
|
||||||
|
periodicEvent?.Set();
|
||||||
|
periodicEvent?.Dispose();
|
||||||
|
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||||
|
_ = UnsubscribeAllAsync();
|
||||||
|
}
|
||||||
|
semaphoreSlim?.Dispose();
|
||||||
|
base.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ namespace CryptoExchange.Net.Converters
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
|
if (reader.TokenType == JsonToken.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (objectType == typeof(JToken))
|
if (objectType == typeof(JToken))
|
||||||
return JToken.Load(reader);
|
return JToken.Load(reader);
|
||||||
|
|
||||||
@@ -106,7 +109,7 @@ namespace CryptoExchange.Net.Converters
|
|||||||
|
|
||||||
if ((property.PropertyType == typeof(decimal)
|
if ((property.PropertyType == typeof(decimal)
|
||||||
|| property.PropertyType == typeof(decimal?))
|
|| property.PropertyType == typeof(decimal?))
|
||||||
&& (value != null && value.ToString().Contains("e")))
|
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||||
{
|
{
|
||||||
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||||
property.SetValue(result, dec);
|
property.SetValue(result, dec);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -32,13 +31,13 @@ namespace CryptoExchange.Net.Converters
|
|||||||
if(reader.TokenType is JsonToken.Integer)
|
if(reader.TokenType is JsonToken.Integer)
|
||||||
{
|
{
|
||||||
var longValue = (long)reader.Value;
|
var longValue = (long)reader.Value;
|
||||||
if (longValue == 0)
|
if (longValue == 0 || longValue == -1)
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||||
if (longValue < 1999999999)
|
if (longValue < 19999999999)
|
||||||
return ConvertFromSeconds(longValue);
|
return ConvertFromSeconds(longValue);
|
||||||
if (longValue < 1999999999999)
|
if (longValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds(longValue);
|
return ConvertFromMilliseconds(longValue);
|
||||||
if (longValue < 1999999999999999)
|
if (longValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds(longValue);
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
return ConvertFromNanoseconds(longValue);
|
||||||
@@ -46,7 +45,10 @@ namespace CryptoExchange.Net.Converters
|
|||||||
else if (reader.TokenType is JsonToken.Float)
|
else if (reader.TokenType is JsonToken.Float)
|
||||||
{
|
{
|
||||||
var doubleValue = (double)reader.Value;
|
var doubleValue = (double)reader.Value;
|
||||||
if (doubleValue < 1999999999)
|
if (doubleValue == 0 || doubleValue == -1)
|
||||||
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
|
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
|
||||||
return ConvertFromMilliseconds(doubleValue);
|
return ConvertFromMilliseconds(doubleValue);
|
||||||
@@ -57,6 +59,9 @@ namespace CryptoExchange.Net.Converters
|
|||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||||
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
if (stringValue.Length == 8)
|
||||||
{
|
{
|
||||||
// Parse 20211103 format
|
// Parse 20211103 format
|
||||||
@@ -86,11 +91,11 @@ namespace CryptoExchange.Net.Converters
|
|||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
{
|
{
|
||||||
// Parse 1637745563.000 format
|
// Parse 1637745563.000 format
|
||||||
if (doubleValue < 1999999999)
|
if (doubleValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(doubleValue);
|
||||||
if (doubleValue < 1999999999999)
|
if (doubleValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
if (doubleValue < 1999999999999999)
|
if (doubleValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace CryptoExchange.Net.Converters
|
|||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var stringValue = GetString(value);
|
var stringValue = GetString(value);
|
||||||
writer.WriteRawValue(stringValue);
|
writer.WriteValue(stringValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||||
<PackageVersion>5.1.7</PackageVersion>
|
<PackageVersion>5.4.0</PackageVersion>
|
||||||
<AssemblyVersion>5.1.7</AssemblyVersion>
|
<AssemblyVersion>5.4.0</AssemblyVersion>
|
||||||
<FileVersion>5.1.7</FileVersion>
|
<FileVersion>5.4.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>5.1.7 - Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients</PackageReleaseNotes>
|
<PackageReleaseNotes>5.4.0 - Added unsubscribing when receiving subscribe answer after the request timeout has passed, Fixed socket options copying, Made TimeSync implementation optional, Cleaned up ApiCredentials and added better support for extending ApiCredentials</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>9.0</LangVersion>
|
<LangVersion>9.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ namespace CryptoExchange.Net
|
|||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
uriBuilder.Host = baseUri.Host;
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
@@ -454,6 +455,7 @@ namespace CryptoExchange.Net
|
|||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
uriBuilder.Host = baseUri.Host;
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base api client
|
||||||
|
/// </summary>
|
||||||
|
public interface IBaseApiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Set the API credentials for this API client
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="credentials"></param>
|
||||||
|
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base rest API client
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestApiClient : IBaseApiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating requests. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
IRequestFactory RequestFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total amount of requests made with this API client
|
||||||
|
/// </summary>
|
||||||
|
int TotalRequestsMade { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
TimeSpan? GetTimeOffset();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
TimeSyncInfo? GetTimeSyncInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,24 +10,13 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
public interface IRestClient: IDisposable
|
public interface IRestClient: IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The factory for creating requests. Used for unit testing
|
/// The options provided for this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IRequestFactory RequestFactory { get; set; }
|
ClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total amount of requests made with this client
|
/// The total amount of requests made with this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int TotalRequestsMade { get; }
|
int TotalRequestsMade { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The options provided for this client
|
|
||||||
/// </summary>
|
|
||||||
BaseRestClientOptions ClientOptions { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
void SetApiCredentials(ApiCredentials credentials);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket API client
|
||||||
|
/// </summary>
|
||||||
|
public interface ISocketApiClient: IBaseApiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of socket connections on the API client
|
||||||
|
/// </summary>
|
||||||
|
int CurrentConnections { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of subscriptions over all connections
|
||||||
|
/// </summary>
|
||||||
|
int CurrentSubscriptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Incoming data kpbs
|
||||||
|
/// </summary>
|
||||||
|
double IncomingKbps { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Client options
|
||||||
|
/// </summary>
|
||||||
|
SocketApiClientOptions Options { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating sockets. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
IWebsocketFactory SocketFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the url to reconnect to after losing a connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
string GetSubscriptionsState();
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect all connections
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task ReconnectAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The original request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CallResult<object>> RevitalizeRequestAsync(object request);
|
||||||
|
/// <summary>
|
||||||
|
/// Periodically sends data over a socket connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">Identifier for the periodic send</param>
|
||||||
|
/// <param name="interval">How often</param>
|
||||||
|
/// <param name="objGetter">Method returning the object to send</param>
|
||||||
|
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe all subscriptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UnsubscribeAllAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> UnsubscribeAsync(int subscriptionId);
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,19 +14,23 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The options provided for this client
|
/// The options provided for this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BaseSocketClientOptions ClientOptions { get; }
|
ClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
void SetApiCredentials(ApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Incoming kilobytes per second of data
|
/// Incoming kilobytes per second of data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double IncomingKbps { get; }
|
public double IncomingKbps { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentConnections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of subscriptions running from the client
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentSubscriptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -27,43 +28,31 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Websocket opened event
|
/// Websocket opened event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action OnOpen;
|
event Action OnOpen;
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket has lost connection to the server and is attempting to reconnect
|
||||||
|
/// </summary>
|
||||||
|
event Action OnReconnecting;
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket has reconnected to the server
|
||||||
|
/// </summary>
|
||||||
|
event Action OnReconnected;
|
||||||
|
/// <summary>
|
||||||
|
/// Get reconntion url
|
||||||
|
/// </summary>
|
||||||
|
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unique id for this socket
|
/// Unique id for this socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int Id { get; }
|
int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Origin header
|
|
||||||
/// </summary>
|
|
||||||
string? Origin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Encoding to use for sending/receiving string data
|
|
||||||
/// </summary>
|
|
||||||
Encoding? Encoding { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Whether socket is in the process of reconnecting
|
|
||||||
/// </summary>
|
|
||||||
bool Reconnecting { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of outgoing messages per second
|
|
||||||
/// </summary>
|
|
||||||
int? RatelimitPerSecond { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
||||||
/// </summary>
|
/// </summary>
|
||||||
double IncomingKbps { get; }
|
double IncomingKbps { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for byte data
|
/// The uri the socket connects to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<byte[], string>? DataInterpreterBytes { get; set; }
|
Uri Uri { get; }
|
||||||
/// <summary>
|
|
||||||
/// Handler for string data
|
|
||||||
/// </summary>
|
|
||||||
Func<string, string>? DataInterpreterString { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The url the socket connects to
|
|
||||||
/// </summary>
|
|
||||||
string Url { get; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the socket connection is closed
|
/// Whether the socket connection is closed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -73,19 +62,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsOpen { get; }
|
bool IsOpen { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Supported ssl protocols
|
|
||||||
/// </summary>
|
|
||||||
SslProtocols SSLProtocols { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max time for no data being received before the connection is considered lost
|
|
||||||
/// </summary>
|
|
||||||
TimeSpan Timeout { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Set a proxy to use when connecting
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="proxy"></param>
|
|
||||||
void SetProxy(ApiProxy proxy);
|
|
||||||
/// <summary>
|
|
||||||
/// Connect the socket
|
/// Connect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -96,9 +72,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
void Send(string data);
|
void Send(string data);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset socket when a connection is lost to prepare for a new connection
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Reset();
|
/// <returns></returns>
|
||||||
|
Task ReconnectAsync();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Close the connection
|
/// Close the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Sockets;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@@ -12,17 +12,8 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Create a websocket for an url
|
/// Create a websocket for an url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="log">The logger</param>
|
/// <param name="log">The logger</param>
|
||||||
/// <param name="url">The url the socket is fo</param>
|
/// <param name="parameters">The parameters to use for the connection</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
IWebsocket CreateWebsocket(Log log, string url);
|
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
|
||||||
/// <summary>
|
|
||||||
/// Create a websocket for an url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">The logger</param>
|
|
||||||
/// <param name="url">The url the socket is fo</param>
|
|
||||||
/// <param name="cookies">Cookies to be send in the initial request</param>
|
|
||||||
/// <param name="headers">Headers to be send in the initial request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace CryptoExchange.Net.Logging
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientName { get; set; }
|
public string ClientName { get; set; }
|
||||||
|
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,7 +44,8 @@ namespace CryptoExchange.Net.Logging
|
|||||||
/// <param name="textWriters"></param>
|
/// <param name="textWriters"></param>
|
||||||
public void UpdateWriters(List<ILogger> textWriters)
|
public void UpdateWriters(List<ILogger> textWriters)
|
||||||
{
|
{
|
||||||
writers = textWriters;
|
lock (_lock)
|
||||||
|
writers = textWriters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,16 +59,19 @@ namespace CryptoExchange.Net.Logging
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var logMessage = $"{ClientName,-10} | {message}";
|
var logMessage = $"{ClientName,-10} | {message}";
|
||||||
foreach (var writer in writers.ToList())
|
lock (_lock)
|
||||||
{
|
{
|
||||||
try
|
foreach (var writer in writers)
|
||||||
{
|
{
|
||||||
writer.Log(logLevel, logMessage);
|
try
|
||||||
}
|
{
|
||||||
catch (Exception e)
|
writer.Log(logLevel, logMessage);
|
||||||
{
|
}
|
||||||
// Can't write to the logging so where else to output..
|
catch (Exception e)
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
{
|
||||||
|
// Can't write to the logging so where else to output..
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,215 +10,89 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace CryptoExchange.Net.Objects
|
namespace CryptoExchange.Net.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base options, applicable to everything
|
/// Client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaseOptions
|
public abstract class ClientOptions
|
||||||
{
|
{
|
||||||
|
internal event Action? OnLoggingChanged;
|
||||||
|
|
||||||
|
private LogLevel _logLevel = LogLevel.Information;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minimum log level to output
|
/// The minimum log level to output
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
public LogLevel LogLevel
|
||||||
|
{
|
||||||
|
get => _logLevel;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logLevel = value;
|
||||||
|
OnLoggingChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The log writers
|
/// The log writers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ILogger> LogWriters { get; set; } = new List<ILogger> { new DebugLogger() };
|
public List<ILogger> LogWriters
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
|
||||||
/// </summary>
|
|
||||||
public bool OutputOriginalData { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public BaseOptions(): this(null)
|
|
||||||
{
|
{
|
||||||
|
get => _logWriters;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logWriters = value;
|
||||||
|
OnLoggingChanged?.Invoke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
|
||||||
public BaseOptions(BaseOptions? baseOptions)
|
|
||||||
{
|
|
||||||
if (baseOptions == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LogLevel = baseOptions.LogLevel;
|
|
||||||
LogWriters = baseOptions.LogWriters.ToList();
|
|
||||||
OutputOriginalData = baseOptions.OutputOriginalData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, OutputOriginalData: {OutputOriginalData}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client options, for both the socket and rest clients
|
|
||||||
/// </summary>
|
|
||||||
public class BaseClientOptions : BaseOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy to use when connecting
|
/// Proxy to use when connecting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiProxy? Proxy { get; set; }
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api credentials to be used for signing requests to private endpoints. These credentials will be used for each API in the client, unless overriden in the API options
|
/// The api credentials used for signing requests to this API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiCredentials? ApiCredentials { get; set; }
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BaseClientOptions() : this(null)
|
public ClientOptions()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
/// <param name="clientOptions">Copy values for the provided options</param>
|
||||||
public BaseClientOptions(BaseClientOptions? baseOptions) : base(baseOptions)
|
public ClientOptions(ClientOptions? clientOptions)
|
||||||
{
|
{
|
||||||
if (baseOptions == null)
|
if (clientOptions == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Proxy = baseOptions.Proxy;
|
LogLevel = clientOptions.LogLevel;
|
||||||
ApiCredentials = baseOptions.ApiCredentials?.Copy();
|
LogWriters = clientOptions.LogWriters.ToList();
|
||||||
|
Proxy = clientOptions.Proxy;
|
||||||
|
ApiCredentials = clientOptions.ApiCredentials?.Copy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseOptions">Copy values for the provided options</param>
|
||||||
|
/// <param name="newValues">Copy values for the provided options</param>
|
||||||
|
internal ClientOptions(ClientOptions baseOptions, ClientOptions? newValues)
|
||||||
|
{
|
||||||
|
Proxy = newValues?.Proxy ?? baseOptions.Proxy;
|
||||||
|
LogLevel = baseOptions.LogLevel;
|
||||||
|
LogWriters = baseOptions.LogWriters.ToList();
|
||||||
|
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{base.ToString()}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}, Base.ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rest client options
|
|
||||||
/// </summary>
|
|
||||||
public class BaseRestClientOptions : BaseClientOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The time the server has to respond to a request before timing out
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
|
|
||||||
/// </summary>
|
|
||||||
public HttpClient? HttpClient { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public BaseRestClientOptions(): this(null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
|
||||||
public BaseRestClientOptions(BaseRestClientOptions? baseOptions): base(baseOptions)
|
|
||||||
{
|
|
||||||
if (baseOptions == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
HttpClient = baseOptions.HttpClient;
|
|
||||||
RequestTimeout = baseOptions.RequestTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Socket client options
|
|
||||||
/// </summary>
|
|
||||||
public class BaseSocketClientOptions : BaseClientOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not the socket should automatically reconnect when losing connection
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time to wait between reconnect attempts
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of times to try to reconnect, default null will retry indefinitely
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxReconnectTries { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of times to try to resubscribe after reconnecting
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxResubscribeTries { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
|
||||||
/// </summary>
|
|
||||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The max time to wait for a response after sending a request on the socket before giving a timeout
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
|
||||||
/// for example when the server sends intermittent ping requests
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
|
||||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
|
||||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
|
||||||
/// </summary>
|
|
||||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public BaseSocketClientOptions(): this(null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
|
||||||
public BaseSocketClientOptions(BaseSocketClientOptions? baseOptions): base(baseOptions)
|
|
||||||
{
|
|
||||||
if (baseOptions == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AutoReconnect = baseOptions.AutoReconnect;
|
|
||||||
ReconnectInterval = baseOptions.ReconnectInterval;
|
|
||||||
MaxReconnectTries = baseOptions.MaxReconnectTries;
|
|
||||||
MaxResubscribeTries = baseOptions.MaxResubscribeTries;
|
|
||||||
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
|
||||||
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
|
|
||||||
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
|
|
||||||
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxReconnectTries: {MaxReconnectTries}, MaxResubscribeTries: {MaxResubscribeTries}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +101,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiClientOptions
|
public class ApiClientOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||||
|
/// </summary>
|
||||||
|
public bool OutputOriginalData { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base address of the API
|
/// The base address of the API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -264,12 +143,13 @@ namespace CryptoExchange.Net.Objects
|
|||||||
{
|
{
|
||||||
BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
|
BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
|
||||||
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||||
|
OutputOriginalData = newValues?.OutputOriginalData ?? baseOptions.OutputOriginalData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
|
return $"OutputOriginalData: {OutputOriginalData}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +158,16 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RestApiClientOptions: ApiClientOptions
|
public class RestApiClientOptions: ApiClientOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time the server has to respond to a request before timing out
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
|
||||||
|
/// </summary>
|
||||||
|
public HttpClient? HttpClient { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of rate limiters to use
|
/// List of rate limiters to use
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -320,6 +210,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="newValues">Copy values for the provided options</param>
|
/// <param name="newValues">Copy values for the provided options</param>
|
||||||
public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
|
public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
|
||||||
{
|
{
|
||||||
|
HttpClient = newValues?.HttpClient ?? baseOn.HttpClient;
|
||||||
|
RequestTimeout = newValues == default ? baseOn.RequestTimeout : newValues.RequestTimeout;
|
||||||
RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
|
RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
|
||||||
AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
|
AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
|
||||||
TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
|
TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
|
||||||
@@ -329,14 +221,104 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{base.ToString()}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
|
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rest API client options
|
||||||
|
/// </summary>
|
||||||
|
public class SocketApiClientOptions : ApiClientOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not the socket should automatically reconnect when losing connection
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoReconnect { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time to wait between reconnect attempts
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||||
|
/// </summary>
|
||||||
|
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max time to wait for a response after sending a request on the socket before giving a timeout
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||||
|
/// for example when the server sends intermittent ping requests
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||||
|
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||||
|
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||||
|
/// </summary>
|
||||||
|
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxSocketConnections { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SocketApiClientOptions()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAddress">Base address for the API</param>
|
||||||
|
public SocketApiClientOptions(string baseAddress) : base(baseAddress)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseOptions">Copy values for the provided options</param>
|
||||||
|
/// <param name="newValues">Copy values for the provided options</param>
|
||||||
|
public SocketApiClientOptions(SocketApiClientOptions baseOptions, SocketApiClientOptions? newValues) : base(baseOptions, newValues)
|
||||||
|
{
|
||||||
|
if (baseOptions == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
|
||||||
|
ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
|
||||||
|
MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||||
|
SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
|
||||||
|
SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
|
||||||
|
SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
|
||||||
|
MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
|
||||||
|
DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for order book options
|
/// Base for order book options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OrderBookOptions : BaseOptions
|
public class OrderBookOptions : ClientOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
set { } }
|
set { } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
|
return bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
return asks.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
|
return asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,24 +258,32 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
}
|
}
|
||||||
|
|
||||||
_subscription = startResult.Data;
|
_subscription = startResult.Data;
|
||||||
_subscription.ConnectionLost += () =>
|
_subscription.ConnectionLost += HandleConnectionLost;
|
||||||
{
|
_subscription.ConnectionClosed += HandleConnectionClosed;
|
||||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
_subscription.ConnectionRestored += HandleConnectionRestored;
|
||||||
Status = OrderBookStatus.Reconnecting;
|
|
||||||
Reset();
|
|
||||||
};
|
|
||||||
_subscription.ConnectionClosed += () =>
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
|
||||||
Status = OrderBookStatus.Disconnected;
|
|
||||||
_ = StopAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
_subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false);
|
|
||||||
Status = OrderBookStatus.Synced;
|
Status = OrderBookStatus.Synced;
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionLost() {
|
||||||
|
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||||
|
if (Status != OrderBookStatus.Disposed) {
|
||||||
|
Status = OrderBookStatus.Reconnecting;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionClosed() {
|
||||||
|
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||||
|
Status = OrderBookStatus.Disconnected;
|
||||||
|
_ = StopAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleConnectionRestored(TimeSpan _) {
|
||||||
|
await ResyncAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
@@ -286,8 +294,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if (_processTask != null)
|
if (_processTask != null)
|
||||||
await _processTask.ConfigureAwait(false);
|
await _processTask.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_subscription != null)
|
if (_subscription != null) {
|
||||||
await _subscription.CloseAsync().ConfigureAwait(false);
|
await _subscription.CloseAsync().ConfigureAwait(false);
|
||||||
|
_subscription.ConnectionLost -= HandleConnectionLost;
|
||||||
|
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||||
|
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||||
|
}
|
||||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +495,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="timeout">Max wait time</param>
|
/// <param name="timeout">Max wait time</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(int timeout, CancellationToken ct)
|
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var startWait = DateTime.UtcNow;
|
var startWait = DateTime.UtcNow;
|
||||||
while (!bookSet && Status == OrderBookStatus.Syncing)
|
while (!bookSet && Status == OrderBookStatus.Syncing)
|
||||||
@@ -491,12 +503,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if(ct.IsCancellationRequested)
|
if(ct.IsCancellationRequested)
|
||||||
return new CallResult<bool>(new CancellationRequestedError());
|
return new CallResult<bool>(new CancellationRequestedError());
|
||||||
|
|
||||||
if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout)
|
if (DateTime.UtcNow - startWait > timeout)
|
||||||
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
|
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(10, ct).ConfigureAwait(false);
|
await Task.Delay(50, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{ }
|
{ }
|
||||||
@@ -569,7 +581,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
var (bestBid, bestAsk) = BestOffers;
|
var (bestBid, bestAsk) = BestOffers;
|
||||||
if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity ||
|
if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity ||
|
||||||
bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity)
|
bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity)
|
||||||
|
{
|
||||||
OnBestOffersChanged?.Invoke((bestBid, bestAsk));
|
OnBestOffersChanged?.Invoke((bestBid, bestAsk));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Reset()
|
private void Reset()
|
||||||
@@ -601,13 +615,13 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
private async Task ProcessQueue()
|
private async Task ProcessQueue()
|
||||||
{
|
{
|
||||||
while (Status != OrderBookStatus.Disconnected)
|
while (Status != OrderBookStatus.Disconnected && Status != OrderBookStatus.Disposed)
|
||||||
{
|
{
|
||||||
await _queueEvent.WaitAsync().ConfigureAwait(false);
|
await _queueEvent.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
while (_processQueue.TryDequeue(out var item))
|
while (_processQueue.TryDequeue(out var item))
|
||||||
{
|
{
|
||||||
if (Status == OrderBookStatus.Disconnected)
|
if (Status == OrderBookStatus.Disconnected || Status == OrderBookStatus.Disposed)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (_stopProcessing)
|
if (_stopProcessing)
|
||||||
@@ -740,7 +754,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
_ = _subscription!.ReconnectAsync();
|
_ = _subscription!.ReconnectAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
await ResyncAsync().ConfigureAwait(false);
|
await ResyncAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Security.Authentication;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -22,29 +20,38 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptoExchangeWebSocketClient : IWebsocket
|
public class CryptoExchangeWebSocketClient : IWebsocket
|
||||||
{
|
{
|
||||||
internal static int lastStreamId;
|
enum ProcessState
|
||||||
private static readonly object streamIdLock = new object();
|
{
|
||||||
|
Idle,
|
||||||
|
Processing,
|
||||||
|
WaitingForClose,
|
||||||
|
Reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int lastStreamId;
|
||||||
|
private static readonly object streamIdLock = new();
|
||||||
|
|
||||||
private ClientWebSocket _socket;
|
|
||||||
private Task? _sendTask;
|
|
||||||
private Task? _receiveTask;
|
|
||||||
private Task? _timeoutTask;
|
|
||||||
private readonly AsyncResetEvent _sendEvent;
|
private readonly AsyncResetEvent _sendEvent;
|
||||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
||||||
private readonly IDictionary<string, string> cookies;
|
private readonly SemaphoreSlim _closeSem;
|
||||||
private readonly IDictionary<string, string> headers;
|
|
||||||
private CancellationTokenSource _ctsSource;
|
|
||||||
private bool _closing;
|
|
||||||
private bool _startedSent;
|
|
||||||
private bool _startedReceive;
|
|
||||||
|
|
||||||
private readonly List<DateTime> _outgoingMessages;
|
private readonly List<DateTime> _outgoingMessages;
|
||||||
|
|
||||||
|
private ClientWebSocket _socket;
|
||||||
|
private CancellationTokenSource _ctsSource;
|
||||||
private DateTime _lastReceivedMessagesUpdate;
|
private DateTime _lastReceivedMessagesUpdate;
|
||||||
|
private Task? _processTask;
|
||||||
|
private Task? _closeTask;
|
||||||
|
private bool _stopRequested;
|
||||||
|
private bool _disposed;
|
||||||
|
private ProcessState _processState;
|
||||||
|
private DateTime _lastReconnectTime;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received messages, the size and the timstamp
|
/// Received messages, the size and the timstamp
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly List<ReceiveItem> _receivedMessages;
|
protected readonly List<ReceiveItem> _receivedMessages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received messages lock
|
/// Received messages lock
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,82 +60,27 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log
|
/// Log
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Log log;
|
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>>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? Origin { get; set; }
|
public WebSocketParameters Parameters { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Reconnecting { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The timestamp this socket has been active for the last time
|
/// The timestamp this socket has been active for the last time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastActionTime { get; private set; }
|
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; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Url { get; }
|
public Uri Uri => Parameters.Uri;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsOpen => _socket.State == WebSocketState.Open && !_closing;
|
public bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION
|
|
||||||
/// </summary>
|
|
||||||
public SslProtocols SSLProtocols { get; set; }
|
|
||||||
|
|
||||||
private Encoding _encoding = Encoding.UTF8;
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Encoding? Encoding
|
|
||||||
{
|
|
||||||
get => _encoding;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(value != null)
|
|
||||||
_encoding = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of outgoing messages per second
|
|
||||||
/// </summary>
|
|
||||||
public int? RatelimitPerSecond { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public TimeSpan Timeout { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
@@ -148,57 +100,31 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action OnClose
|
public event Action? OnClose;
|
||||||
{
|
|
||||||
add => closeHandlers.Add(value);
|
|
||||||
remove => closeHandlers.Remove(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<string> OnMessage
|
public event Action<string>? OnMessage;
|
||||||
{
|
|
||||||
add => messageHandlers.Add(value);
|
|
||||||
remove => messageHandlers.Remove(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<Exception> OnError
|
public event Action<Exception>? OnError;
|
||||||
{
|
|
||||||
add => errorHandlers.Add(value);
|
|
||||||
remove => errorHandlers.Remove(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action OnOpen
|
public event Action? OnOpen;
|
||||||
{
|
/// <inheritdoc />
|
||||||
add => openHandlers.Add(value);
|
public event Action? OnReconnecting;
|
||||||
remove => openHandlers.Remove(value);
|
/// <inheritdoc />
|
||||||
}
|
public event Action? OnReconnected;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="log">The log object to use</param>
|
/// <param name="log">The log object to use</param>
|
||||||
/// <param name="url">The url the socket should connect to</param>
|
/// <param name="websocketParameters">The parameters for this socket</param>
|
||||||
public CryptoExchangeWebSocketClient(Log log, string url) : this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>())
|
public CryptoExchangeWebSocketClient(Log log, WebSocketParameters websocketParameters)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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();
|
Id = NextStreamId();
|
||||||
this.log = log;
|
_log = log;
|
||||||
Url = url;
|
|
||||||
this.cookies = cookies;
|
|
||||||
this.headers = headers;
|
|
||||||
|
|
||||||
|
Parameters = websocketParameters;
|
||||||
_outgoingMessages = new List<DateTime>();
|
_outgoingMessages = new List<DateTime>();
|
||||||
_receivedMessages = new List<ReceiveItem>();
|
_receivedMessages = new List<ReceiveItem>();
|
||||||
_sendEvent = new AsyncResetEvent();
|
_sendEvent = new AsyncResetEvent();
|
||||||
@@ -206,112 +132,238 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
_receivedMessagesLock = new object();
|
_receivedMessagesLock = new object();
|
||||||
|
|
||||||
|
_closeSem = new SemaphoreSlim(1, 1);
|
||||||
_socket = CreateSocket();
|
_socket = CreateSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetProxy(ApiProxy proxy)
|
|
||||||
{
|
|
||||||
Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri);
|
|
||||||
_socket.Options.Proxy = uri?.Scheme == null
|
|
||||||
? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
|
||||||
: _socket.Options.Proxy = new WebProxy
|
|
||||||
{
|
|
||||||
Address = uri
|
|
||||||
};
|
|
||||||
if (proxy.Login != null)
|
|
||||||
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task<bool> ConnectAsync()
|
public virtual async Task<bool> ConnectAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
if (!await ConnectInternalAsync().ConfigureAwait(false))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
OnOpen?.Invoke();
|
||||||
|
_processTask = ProcessAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the socket object
|
||||||
|
/// </summary>
|
||||||
|
private ClientWebSocket CreateSocket()
|
||||||
|
{
|
||||||
|
var cookieContainer = new CookieContainer();
|
||||||
|
foreach (var cookie in Parameters.Cookies)
|
||||||
|
cookieContainer.Add(new Cookie(cookie.Key, cookie.Value));
|
||||||
|
|
||||||
|
var socket = new ClientWebSocket();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
socket.Options.Cookies = cookieContainer;
|
||||||
await _socket.ConnectAsync(new Uri(Url), tcs.Token).ConfigureAwait(false);
|
foreach (var header in Parameters.Headers)
|
||||||
|
socket.Options.SetRequestHeader(header.Key, header.Value);
|
||||||
Handle(openHandlers);
|
socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero;
|
||||||
|
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
|
||||||
|
if (Parameters.Proxy != null)
|
||||||
|
SetProxy(socket, Parameters.Proxy);
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException)
|
||||||
|
{
|
||||||
|
// Options are not supported on certain platforms (WebAssembly for instance)
|
||||||
|
// best we can do it try to connect without setting options.
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ConnectInternalAsync()
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
||||||
|
await _socket.ConnectAsync(Uri, tcs.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
_log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} connection succeeded, starting communication");
|
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||||
_sendTask = Task.Factory.StartNew(SendLoopAsync, default, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
|
return true;
|
||||||
_receiveTask = Task.Factory.StartNew(ReceiveLoopAsync, default, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
|
}
|
||||||
if (Timeout != default)
|
|
||||||
_timeoutTask = Task.Run(CheckTimeoutAsync);
|
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
/// <inheritdoc />
|
||||||
while (!_startedSent || !_startedReceive)
|
private async Task ProcessAsync()
|
||||||
|
{
|
||||||
|
while (!_stopRequested)
|
||||||
{
|
{
|
||||||
// Wait for the tasks to have actually started
|
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks");
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
_processState = ProcessState.Processing;
|
||||||
|
var sendTask = SendLoopAsync();
|
||||||
|
var receiveTask = ReceiveLoopAsync();
|
||||||
|
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||||
|
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} processing tasks finished");
|
||||||
|
|
||||||
if(sw.ElapsedMilliseconds > 5000)
|
_processState = ProcessState.WaitingForClose;
|
||||||
|
while (_closeTask == null)
|
||||||
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
_closeTask = null;
|
||||||
|
|
||||||
|
if (!Parameters.AutoReconnect)
|
||||||
{
|
{
|
||||||
_ = _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", default);
|
_processState = ProcessState.Idle;
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} startup interupted");
|
OnClose?.Invoke();
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_stopRequested)
|
||||||
|
{
|
||||||
|
_processState = ProcessState.Reconnecting;
|
||||||
|
OnReconnecting?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||||
|
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
||||||
|
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
||||||
|
|
||||||
|
while (!_stopRequested)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
|
||||||
|
var task = GetReconnectionUrl?.Invoke();
|
||||||
|
if (task != null)
|
||||||
|
{
|
||||||
|
var reconnectUri = await task.ConfigureAwait(false);
|
||||||
|
if (reconnectUri != null && Parameters.Uri != reconnectUri)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
|
||||||
|
Parameters.Uri = reconnectUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_socket = CreateSocket();
|
||||||
|
_ctsSource.Dispose();
|
||||||
|
_ctsSource = new CancellationTokenSource();
|
||||||
|
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||||
|
|
||||||
|
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||||
|
if (!connected)
|
||||||
|
{
|
||||||
|
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastReconnectTime = DateTime.UtcNow;
|
||||||
|
OnReconnected?.Invoke();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connected to {Url}");
|
_processState = ProcessState.Idle;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(string data)
|
public virtual void Send(string data)
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
throw new InvalidOperationException($"Socket {Id} Can't send data when socket is not connected");
|
return;
|
||||||
|
|
||||||
var bytes = _encoding.GetBytes(data);
|
var bytes = Parameters.Encoding.GetBytes(data);
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
_log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
||||||
_sendBuffer.Enqueue(bytes);
|
_sendBuffer.Enqueue(bytes);
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task CloseAsync()
|
public virtual async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
if (_processState != ProcessState.Processing && IsOpen)
|
||||||
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;
|
return;
|
||||||
|
|
||||||
_closing = true;
|
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
|
||||||
var tasksToAwait = new List<Task>();
|
_closeTask = CloseInternalAsync();
|
||||||
if (_socket.State == WebSocketState.Open)
|
await _closeTask.ConfigureAwait(false);
|
||||||
tasksToAwait.Add(_socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default));
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task CloseAsync()
|
||||||
|
{
|
||||||
|
await _closeSem.WaitAsync().ConfigureAwait(false);
|
||||||
|
_stopRequested = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_closeTask?.IsCompleted == false)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsOpen)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_closeSem.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
if(_processTask != null)
|
||||||
|
await _processTask.ConfigureAwait(false);
|
||||||
|
OnClose?.Invoke();
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal close method
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task CloseInternalAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//_closeState = CloseState.Closing;
|
||||||
_ctsSource.Cancel();
|
_ctsSource.Cancel();
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
if (waitSend)
|
|
||||||
tasksToAwait.Add(_sendTask!);
|
|
||||||
if (waitReceive)
|
|
||||||
tasksToAwait.Add(_receiveTask!);
|
|
||||||
if (_timeoutTask != null)
|
|
||||||
tasksToAwait.Add(_timeoutTask);
|
|
||||||
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} waiting for communication loops to finish");
|
if (_socket.State == WebSocketState.Open)
|
||||||
await Task.WhenAll(tasksToAwait).ConfigureAwait(false);
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
try
|
||||||
Handle(closeHandlers);
|
{
|
||||||
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||||
|
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||||
|
// So socket might go to aborted state, might still be open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(_socket.State == WebSocketState.CloseReceived)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||||
|
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||||
|
// So socket might go to aborted state, might still be open
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -319,45 +371,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} disposing");
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} disposing");
|
||||||
|
_disposed = true;
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
_ctsSource.Dispose();
|
_ctsSource.Dispose();
|
||||||
|
_log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
||||||
errorHandlers.Clear();
|
|
||||||
openHandlers.Clear();
|
|
||||||
closeHandlers.Clear();
|
|
||||||
messageHandlers.Clear();
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} resetting");
|
|
||||||
_ctsSource = new CancellationTokenSource();
|
|
||||||
_closing = false;
|
|
||||||
|
|
||||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
|
||||||
|
|
||||||
_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>
|
/// <summary>
|
||||||
@@ -366,40 +387,39 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task SendLoopAsync()
|
private async Task SendLoopAsync()
|
||||||
{
|
{
|
||||||
_startedSent = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
while (_sendBuffer.TryDequeue(out var data))
|
while (_sendBuffer.TryDequeue(out var data))
|
||||||
{
|
{
|
||||||
if (RatelimitPerSecond != null)
|
if (Parameters.RatelimitPerSecond != null)
|
||||||
{
|
{
|
||||||
// Wait for rate limit
|
// Wait for rate limit
|
||||||
DateTime? start = null;
|
DateTime? start = null;
|
||||||
while (MessagesSentLastSecond() >= RatelimitPerSecond)
|
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
|
||||||
{
|
{
|
||||||
start ??= DateTime.UtcNow;
|
start ??= DateTime.UtcNow;
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start != null)
|
if (start != null)
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
_log.Write(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||||
_outgoingMessages.Add(DateTime.UtcNow);
|
_outgoingMessages.Add(DateTime.UtcNow);
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
_log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -408,9 +428,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
catch (Exception ioe)
|
catch (Exception ioe)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly, .NET framework
|
// Connection closed unexpectedly, .NET framework
|
||||||
Handle(errorHandlers, ioe);
|
OnError?.Invoke(ioe);
|
||||||
_ = Task.Run(async () => await CloseInternalAsync(false, true).ConfigureAwait(false));
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,13 +442,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||||
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
|
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
Handle(errorHandlers, e);
|
_log.Write(LogLevel.Warning, $"Socket {Id} Send loop stopped with exception");
|
||||||
|
OnError?.Invoke(e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} Send loop finished");
|
_log.Write(LogLevel.Debug, $"Socket {Id} Send loop finished");
|
||||||
_startedSent = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,14 +458,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task ReceiveLoopAsync()
|
private async Task ReceiveLoopAsync()
|
||||||
{
|
{
|
||||||
_startedReceive = true;
|
|
||||||
var buffer = new ArraySegment<byte>(new byte[65536]);
|
var buffer = new ArraySegment<byte>(new byte[65536]);
|
||||||
var received = 0;
|
var received = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
MemoryStream? memoryStream = null;
|
MemoryStream? memoryStream = null;
|
||||||
@@ -466,17 +486,19 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
catch (Exception wse)
|
catch (Exception wse)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly
|
// Connection closed unexpectedly
|
||||||
Handle(errorHandlers, wse);
|
OnError?.Invoke(wse);
|
||||||
_ = Task.Run(async () => await CloseInternalAsync(true, true).ConfigureAwait(false));
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly
|
// Connection closed unexpectedly
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
||||||
_ = Task.Run(async () => await CloseInternalAsync(true, true).ConfigureAwait(false));
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +507,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// We received data, but it is not complete, write it to a memory stream for reassembling
|
// We received data, but it is not complete, write it to a memory stream for reassembling
|
||||||
multiPartMessage = true;
|
multiPartMessage = true;
|
||||||
memoryStream ??= new MemoryStream();
|
memoryStream ??= new MemoryStream();
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||||
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -493,13 +515,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!multiPartMessage)
|
if (!multiPartMessage)
|
||||||
{
|
{
|
||||||
// Received a complete message and it's not multi part
|
// Received a complete message and it's not multi part
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
||||||
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Received the end of a multipart message, write to memory stream for reassembling
|
// Received the end of a multipart message, write to memory stream for reassembling
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||||
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -515,7 +537,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiveResult == null || _closing)
|
if (receiveResult == null || _ctsSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Error during receiving or cancellation requested, stop.
|
// Error during receiving or cancellation requested, stop.
|
||||||
break;
|
break;
|
||||||
@@ -527,12 +549,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (receiveResult?.EndOfMessage == true)
|
if (receiveResult?.EndOfMessage == true)
|
||||||
{
|
{
|
||||||
// Reassemble complete message from memory stream
|
// Reassemble complete message from memory stream
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
_log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
||||||
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
|
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
|
||||||
memoryStream.Dispose();
|
memoryStream.Dispose();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
_log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,13 +563,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||||
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
|
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
Handle(errorHandlers, e);
|
_log.Write(LogLevel.Warning, $"Socket {Id} Receive loop stopped with exception");
|
||||||
|
OnError?.Invoke(e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} Receive loop finished");
|
_log.Write(LogLevel.Debug, $"Socket {Id} Receive loop finished");
|
||||||
_startedReceive = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,65 +585,103 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
string strData;
|
string strData;
|
||||||
if (messageType == WebSocketMessageType.Binary)
|
if (messageType == WebSocketMessageType.Binary)
|
||||||
{
|
{
|
||||||
if (DataInterpreterBytes == null)
|
if (Parameters.DataInterpreterBytes == null)
|
||||||
throw new Exception("Byte interpreter not set while receiving byte data");
|
throw new Exception("Byte interpreter not set while receiving byte data");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var relevantData = new byte[count];
|
var relevantData = new byte[count];
|
||||||
Array.Copy(data, offset, relevantData, 0, count);
|
Array.Copy(data, offset, relevantData, 0, count);
|
||||||
strData = DataInterpreterBytes(relevantData);
|
strData = Parameters.DataInterpreterBytes(relevantData);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
strData = _encoding.GetString(data, offset, count);
|
strData = Parameters.Encoding.GetString(data, offset, count);
|
||||||
|
|
||||||
if (DataInterpreterString != null)
|
if (Parameters.DataInterpreterString != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
strData = DataInterpreterString(strData);
|
strData = Parameters.DataInterpreterString(strData);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Handle(messageHandlers, strData);
|
LastActionTime = DateTime.UtcNow;
|
||||||
|
OnMessage?.Invoke(strData);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnMessage event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected void TriggerOnMessage(string data)
|
||||||
|
{
|
||||||
|
LastActionTime = DateTime.UtcNow;
|
||||||
|
OnMessage?.Invoke(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnError event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex"></param>
|
||||||
|
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnError event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnOpen() => OnOpen?.Invoke();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnError event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnClose() => OnClose?.Invoke();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnReconnecting event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnReconnected event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if there is no data received for a period longer than the specified timeout
|
/// Checks if there is no data received for a period longer than the specified timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected async Task CheckTimeoutAsync()
|
protected async Task CheckTimeoutAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Timeout}");
|
_log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
|
||||||
|
LastActionTime = DateTime.UtcNow;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (DateTime.UtcNow - LastActionTime > Timeout)
|
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Timeout}, reconnecting socket");
|
_log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
|
||||||
_ = CloseAsync().ConfigureAwait(false);
|
_ = ReconnectAsync().ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -640,35 +700,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||||
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
Handle(errorHandlers, e);
|
OnError?.Invoke(e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Get the next identifier
|
/// Get the next identifier
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -704,6 +740,28 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_lastReceivedMessagesUpdate = checkTime;
|
_lastReceivedMessagesUpdate = checkTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set proxy on socket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="socket"></param>
|
||||||
|
/// <param name="proxy"></param>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
protected virtual void SetProxy(ClientWebSocket socket, ApiProxy proxy)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri))
|
||||||
|
throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy));
|
||||||
|
|
||||||
|
socket.Options.Proxy = uri?.Scheme == null
|
||||||
|
? socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
||||||
|
: socket.Options.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = uri
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proxy.Login != null)
|
||||||
|
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
|
|
||||||
internal DataEvent(T data, DateTime timestamp)
|
/// <summary>
|
||||||
|
/// Ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
public DataEvent(T data, DateTime timestamp)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
internal class PendingRequest
|
||||||
|
{
|
||||||
|
public Func<JToken, bool> Handler { get; }
|
||||||
|
public JToken? Result { get; private set; }
|
||||||
|
public bool Completed { get; private set; }
|
||||||
|
public AsyncResetEvent Event { get; }
|
||||||
|
public DateTime RequestTimestamp { get; set; }
|
||||||
|
public TimeSpan Timeout { get; }
|
||||||
|
public SocketSubscription? Subscription { get; }
|
||||||
|
|
||||||
|
private CancellationTokenSource cts;
|
||||||
|
|
||||||
|
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
|
||||||
|
{
|
||||||
|
Handler = handler;
|
||||||
|
Event = new AsyncResetEvent(false, false);
|
||||||
|
Timeout = timeout;
|
||||||
|
RequestTimestamp = DateTime.UtcNow;
|
||||||
|
Subscription = subscription;
|
||||||
|
|
||||||
|
cts = new CancellationTokenSource(timeout);
|
||||||
|
cts.Token.Register(Fail, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckData(JToken data)
|
||||||
|
{
|
||||||
|
return Handler(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Succeed(JToken data)
|
||||||
|
{
|
||||||
|
Result = data;
|
||||||
|
Completed = true;
|
||||||
|
Event.Set();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fail()
|
||||||
|
{
|
||||||
|
Completed = true;
|
||||||
|
Event.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -43,19 +43,30 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Confirmed { get; set; }
|
public bool Confirmed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether authentication is needed for this subscription
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we're closing this subscription and a socket connection shouldn't be kept open for it
|
||||||
|
/// </summary>
|
||||||
|
public bool Closed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
|
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
|
||||||
/// a provided cancelation token
|
/// a provided cancelation token
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
|
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
|
||||||
|
|
||||||
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, Action<MessageEvent> dataHandler)
|
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, bool authenticated, Action<MessageEvent> dataHandler)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
UserSubscription = userSubscription;
|
UserSubscription = userSubscription;
|
||||||
MessageHandler = dataHandler;
|
MessageHandler = dataHandler;
|
||||||
Request = request;
|
Request = request;
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
|
Authenticated = authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,12 +75,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <param name="userSubscription"></param>
|
/// <param name="userSubscription"></param>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
/// <param name="dataHandler"></param>
|
/// <param name="dataHandler"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription,
|
public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription,
|
||||||
Action<MessageEvent> dataHandler)
|
bool authenticated, Action<MessageEvent> dataHandler)
|
||||||
{
|
{
|
||||||
return new SocketSubscription(id, request, null, userSubscription, dataHandler);
|
return new SocketSubscription(id, request, null, userSubscription, authenticated, dataHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,12 +90,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <param name="identifier"></param>
|
/// <param name="identifier"></param>
|
||||||
/// <param name="userSubscription"></param>
|
/// <param name="userSubscription"></param>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
/// <param name="dataHandler"></param>
|
/// <param name="dataHandler"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription,
|
public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription,
|
||||||
Action<MessageEvent> dataHandler)
|
bool authenticated, Action<MessageEvent> dataHandler)
|
||||||
{
|
{
|
||||||
return new SocketSubscription(id, null, identifier, userSubscription, dataHandler);
|
return new SocketSubscription(id, null, identifier, userSubscription, authenticated, dataHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the <see cref="BaseSocketClientOptions.MaxReconnectTries"/> and <see cref="BaseSocketClientOptions.MaxResubscribeTries"/> options,
|
/// Event when the connection is closed and will not be reconnected
|
||||||
/// or <see cref="BaseSocketClientOptions.AutoReconnect"/> is false. The socket will not be reconnected
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ConnectionClosed
|
public event Action ConnectionClosed
|
||||||
{
|
{
|
||||||
@@ -72,7 +71,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The id of the socket
|
/// The id of the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SocketId => connection.Socket.Id;
|
public int SocketId => connection.SocketId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The id of the subscription
|
/// The id of the subscription
|
||||||
@@ -103,9 +102,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Close the socket to cause a reconnect
|
/// Close the socket to cause a reconnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal Task ReconnectAsync()
|
public Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
return connection.Socket.CloseAsync();
|
return connection.TriggerReconnectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parameters for a websocket
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketParameters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The uri to connect to
|
||||||
|
/// </summary>
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Headers to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// Cookies to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// The time to wait between reconnect attempts
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy for the connection
|
||||||
|
/// </summary>
|
||||||
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the socket should automatically reconnect when connection is lost
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoReconnect { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? Timeout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Interval at which to send ping frames
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of messages to send per second
|
||||||
|
/// </summary>
|
||||||
|
public int? RatelimitPerSecond { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Origin header value to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public string? Origin { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, string>? DataInterpreterString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encoding for sending/receiving data
|
||||||
|
/// </summary>
|
||||||
|
public Encoding Encoding { get; set; } = Encoding.UTF8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">Uri</param>
|
||||||
|
/// <param name="autoReconnect">Auto reconnect</param>
|
||||||
|
public WebSocketParameters(Uri uri, bool autoReconnect)
|
||||||
|
{
|
||||||
|
Uri = uri;
|
||||||
|
AutoReconnect = autoReconnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
using System.Collections.Generic;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default weboscket factory implementation
|
/// Default websocket factory implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WebsocketFactory : IWebsocketFactory
|
public class WebsocketFactory : IWebsocketFactory
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IWebsocket CreateWebsocket(Log log, string url)
|
public IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters)
|
||||||
{
|
{
|
||||||
return new CryptoExchangeWebSocketClient(log, url);
|
return new CryptoExchangeWebSocketClient(log, parameters);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
|
||||||
{
|
|
||||||
return new CryptoExchangeWebSocketClient(log, url, cookies, headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,103 @@ CryptoExchange.Net is a base package which can be used to easily implement crypt
|
|||||||
## Discord
|
## 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.
|
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.
|
||||||
|
|
||||||
## Donate / Sponsor
|
## Support the project
|
||||||
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.
|
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
|
||||||
|
|
||||||
|
### Referral link
|
||||||
|
Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
|
||||||
|
[Binance](https://accounts.binance.com/en/register?ref=10153680)
|
||||||
|
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
|
||||||
|
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||||
|
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||||
|
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||||
|
[FTX](https://ftx.com/referrals#a=31620192)
|
||||||
|
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||||
|
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||||
|
|
||||||
|
### Donate
|
||||||
|
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||||
|
|
||||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||||
|
|
||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf)
|
### Sponsor
|
||||||
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 5.4.0 - 14 Feb 2023
|
||||||
|
* Added unsubscribing when receiving subscribe answer after the request timeout has passed
|
||||||
|
* Fixed socket options copying
|
||||||
|
* Made TimeSync implementation optional
|
||||||
|
* Cleaned up ApiCredentials and added better support for extending ApiCredentials
|
||||||
|
|
||||||
|
* Version 5.3.1 - 08 Dec 2022
|
||||||
|
* Added default request parameter ordering before applying authentication
|
||||||
|
* Fixed possible issue where a socket would reconnect when it should close if it was already in reconnecting
|
||||||
|
|
||||||
|
* Version 5.3.0 - 14 Nov 2022
|
||||||
|
* Reworked client architecture, shifting funcationality to the ApiClient
|
||||||
|
* Fixed ArrayConverter exponent parsing
|
||||||
|
* Fixed ArrayConverter not checking null
|
||||||
|
* Added optional delay setting after establishing socket connection
|
||||||
|
* Added callback for revitalizing a socket request when reconnecting
|
||||||
|
* Fixed proxy setting websocket
|
||||||
|
|
||||||
|
* Version 5.2.4 - 31 Jul 2022
|
||||||
|
* Added handling of PlatformNotSupportedException when trying to use websocket from WebAssembly
|
||||||
|
* Changed DataEvent to have a public constructor for testing purposes
|
||||||
|
* Fixed EnumConverter serializing values without proper quotes
|
||||||
|
* Fixed websocket connection reconnecting too quickly when resubscribing/reauthenticating fails
|
||||||
|
|
||||||
|
* Version 5.2.3 - 19 Jul 2022
|
||||||
|
* Fixed socket getting disconnected when `no data` timeout is reached instead of being reconnected
|
||||||
|
|
||||||
|
* Version 5.2.2 - 17 Jul 2022
|
||||||
|
* Added support for retrieving a new url when socket connection is lost and reconnection will happen
|
||||||
|
|
||||||
|
* Version 5.2.1 - 16 Jul 2022
|
||||||
|
* Fixed socket reconnect issue
|
||||||
|
* Fixed `message not handled` messages after unsubscribing
|
||||||
|
* Fixed error returning for non-json error responses
|
||||||
|
|
||||||
|
* Version 5.2.0 - 10 Jul 2022
|
||||||
|
* Refactored websocket code, removed some clutter and simplified
|
||||||
|
* Added ReconnectAsync and GetSubscriptionsState methods on socket clients
|
||||||
|
|
||||||
|
* Version 5.1.12 - 12 Jun 2022
|
||||||
|
* Changed time sync so requests no longer wait for it to complete unless it's the first time
|
||||||
|
* Made log client options changable after client creation
|
||||||
|
* Fixed proxy setting not used when reconnecting socket
|
||||||
|
* Changed MaxSocketConnections to a client options
|
||||||
|
* Updated socket reconnection logic
|
||||||
|
|
||||||
|
* Version 5.1.12 - 12 Jun 2022
|
||||||
|
* Changed time sync so requests no longer wait for it to complete unless it's the first time
|
||||||
|
* Made log client options changable after client creation
|
||||||
|
* Fixed proxy setting not used when reconnecting socket
|
||||||
|
* Updated socket reconnection logic
|
||||||
|
|
||||||
|
* Version 5.1.11 - 24 May 2022
|
||||||
|
* Added KeepAliveInterval setting
|
||||||
|
* Fixed port not being copied when setting parameters on request
|
||||||
|
* Fixed inconsistent PackageReference casing in csproj
|
||||||
|
|
||||||
|
* Version 5.1.10 - 22 May 2022
|
||||||
|
* Fixed order book reconnecting while Diposed
|
||||||
|
* Fixed exception when disposing socket client while reconnecting
|
||||||
|
* Added additional null/default checking in DateTimeConverter
|
||||||
|
* Changed ConnectionLost subscription event to run in seperate task to prevent exception/longer operations from intervering with reconnecting
|
||||||
|
|
||||||
|
* Version 5.1.9 - 08 May 2022
|
||||||
|
* Added latency to the timesync calculation
|
||||||
|
* Small fix for exception in socket close handling
|
||||||
|
|
||||||
|
* Version 5.1.8 - 01 May 2022
|
||||||
|
* Cleanup socket code, fixed an issue which could cause connections to never reconnect when connection was lost
|
||||||
|
* Added support for sending requests which expect an empty response
|
||||||
|
* Fixed issue with the DateTimeConverter date interpretation
|
||||||
|
|
||||||
* Version 5.1.7 - 14 Apr 2022
|
* Version 5.1.7 - 14 Apr 2022
|
||||||
* Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients
|
* Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ All updates are wrapped in a `DataEvent<>` object, which contain a `Timestamp`,
|
|||||||
*[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.*
|
*[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.*
|
||||||
|
|
||||||
### Processing subscribe responses
|
### Processing subscribe responses
|
||||||
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same was as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
||||||
```csharp
|
```csharp
|
||||||
|
|
||||||
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||||
|
|||||||
+4
-1
@@ -61,4 +61,7 @@ var client = new BinanceClient(new BinanceClientOptions
|
|||||||
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### How are timezones handled / Timestamps are off by xx
|
||||||
|
Exchange API's treat all timestamps as UTC, both incoming and outgoing. The client libraries do no conversion so be sure to use UTC as well.
|
||||||
+18
-3
@@ -42,11 +42,26 @@ These might not be compatible with other libraries, make sure to check the Crypt
|
|||||||
## Discord
|
## 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.
|
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.
|
||||||
|
|
||||||
## Donate / Sponsor
|
## Support the project
|
||||||
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.
|
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
|
||||||
|
|
||||||
|
### Referral link
|
||||||
|
Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
|
||||||
|
[Binance](https://accounts.binance.com/en/register?ref=10153680)
|
||||||
|
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
|
||||||
|
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||||
|
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||||
|
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||||
|
[FTX](https://ftx.com/referrals#a=31620192)
|
||||||
|
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||||
|
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||||
|
|
||||||
|
### Donate
|
||||||
|
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||||
|
|
||||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||||
|
|
||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf)
|
### Sponsor
|
||||||
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
Reference in New Issue
Block a user