mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-09 00:46:19 +00:00
Initial v2 changes
This commit is contained in:
parent
c558f25b6c
commit
5c63941f32
@ -174,7 +174,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TestImplementation client;
|
||||
if (withOptions)
|
||||
{
|
||||
var options = new ExchangeOptions()
|
||||
var options = new ClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("Test", "Test2"),
|
||||
LogVerbosity = verbosity
|
||||
@ -219,7 +219,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
factory.Setup(c => c.Create(It.IsAny<string>()))
|
||||
.Returns(request.Object);
|
||||
|
||||
TestImplementation client = credentials ? new TestImplementation(new ExchangeOptions() { ApiCredentials = new ApiCredentials("Test", "Test2") }) : new TestImplementation();
|
||||
TestImplementation client = credentials ? new TestImplementation(new ClientOptions() { ApiCredentials = new ApiCredentials("Test", "Test2") }) : new TestImplementation();
|
||||
client.RequestFactory = factory.Object;
|
||||
return client;
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public class TestImplementation: ExchangeClient
|
||||
public class TestImplementation: RestClient
|
||||
{
|
||||
public TestImplementation(): base(new ExchangeOptions(), null) { }
|
||||
public TestImplementation(): base(new ClientOptions(), null) { }
|
||||
|
||||
public TestImplementation(ExchangeOptions exchangeOptions) : base(exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestImplementation(ClientOptions exchangeOptions) : base(exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
{
|
||||
}
|
||||
|
||||
|
244
CryptoExchange.Net/BaseClient.cs
Normal file
244
CryptoExchange.Net/BaseClient.cs
Normal file
@ -0,0 +1,244 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
public abstract class BaseClient
|
||||
{
|
||||
protected string baseAddress;
|
||||
protected Log log;
|
||||
protected ApiProxy apiProxy;
|
||||
protected AuthenticationProvider authProvider;
|
||||
|
||||
protected static int lastId;
|
||||
protected static object idLock = new object();
|
||||
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings()
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
||||
});
|
||||
|
||||
public BaseClient(ExchangeOptions options, AuthenticationProvider authenticationProvider)
|
||||
{
|
||||
log = new Log();
|
||||
authProvider = authenticationProvider;
|
||||
Configure(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the client using the provided options
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions">Options</param>
|
||||
protected virtual void Configure(ExchangeOptions exchangeOptions)
|
||||
{
|
||||
log.UpdateWriters(exchangeOptions.LogWriters);
|
||||
log.Level = exchangeOptions.LogVerbosity;
|
||||
|
||||
baseAddress = exchangeOptions.BaseAddress;
|
||||
apiProxy = exchangeOptions.Proxy;
|
||||
if (apiProxy != null)
|
||||
log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the authentication provider
|
||||
/// </summary>
|
||||
/// <param name="authentictationProvider"></param>
|
||||
protected void SetAuthenticationProvider(AuthenticationProvider authentictationProvider)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Setting api credentials");
|
||||
authProvider = authentictationProvider;
|
||||
}
|
||||
|
||||
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer serializer = null) where T : class
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
var obj = JToken.Parse(data);
|
||||
if (checkObject && log.Level == LogVerbosity.Debug)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (obj is JObject o)
|
||||
{
|
||||
CheckObject(typeof(T), o);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ary = (JArray)obj;
|
||||
if (ary.HasValues && ary[0] is JObject jObject)
|
||||
CheckObject(typeof(T).GetElementType(), jObject);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Failed to check response data: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckObject(Type type, JObject obj)
|
||||
{
|
||||
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
|
||||
// If type has a custom JsonConverter we assume this will handle property mapping
|
||||
return;
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
return;
|
||||
|
||||
if (!obj.HasValues && type != typeof(object))
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, $"Expected `{type.Name}`, but received object was empty");
|
||||
return;
|
||||
}
|
||||
|
||||
bool isDif = false;
|
||||
var properties = new List<string>();
|
||||
var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
|
||||
if (ignore != null)
|
||||
continue;
|
||||
|
||||
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName);
|
||||
}
|
||||
foreach (var token in obj)
|
||||
{
|
||||
var d = properties.SingleOrDefault(p => p == token.Key);
|
||||
if (d == null)
|
||||
{
|
||||
d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower());
|
||||
if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
|
||||
isDif = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
properties.Remove(d);
|
||||
|
||||
var propType = GetProperty(d, props)?.PropertyType;
|
||||
if (propType == null)
|
||||
continue;
|
||||
if (!IsSimple(propType) && propType != typeof(DateTime))
|
||||
{
|
||||
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
|
||||
CheckObject(propType.GetElementType(), (JObject)token.Value[0]);
|
||||
else if (token.Value is JObject)
|
||||
CheckObject(propType, (JObject)token.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var propInfo = props.First(p => p.Name == prop ||
|
||||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
|
||||
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
|
||||
if (optional != null)
|
||||
continue;
|
||||
|
||||
isDif = true;
|
||||
log.Write(LogVerbosity.Warning, $"Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
|
||||
}
|
||||
|
||||
if (isDif)
|
||||
log.Write(LogVerbosity.Debug, "Returned data: " + obj);
|
||||
}
|
||||
|
||||
private PropertyInfo GetProperty(string name, PropertyInfo[] props)
|
||||
{
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
if (attr == null)
|
||||
{
|
||||
if (prop.Name.ToLower() == name.ToLower())
|
||||
return prop;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (((JsonPropertyAttribute)attr).PropertyName == name)
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
protected int NextId()
|
||||
{
|
||||
lock (idLock)
|
||||
{
|
||||
lastId += 1;
|
||||
return lastId;
|
||||
}
|
||||
}
|
||||
|
||||
protected static string FillPathParameter(string endpoint, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
int index = endpoint.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
endpoint = endpoint.Remove(index, 2);
|
||||
endpoint = endpoint.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
authProvider?.Credentials?.Dispose();
|
||||
log.Write(LogVerbosity.Debug, "Disposing exchange client");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
@ -30,16 +31,16 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
public static string CreateParamString(this Dictionary<string, object> parameters)
|
||||
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues)
|
||||
{
|
||||
var uriString = "?";
|
||||
var uriString = "";
|
||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||
foreach (var arrayEntry in arraysParameters)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])arrayEntry.Value).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
}
|
||||
|
||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={s.Value}"))}";
|
||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? WebUtility.UrlEncode(s.Value.ToString()) : s.Value)}"))}";
|
||||
uriString = uriString.TrimEnd('&');
|
||||
return uriString;
|
||||
}
|
||||
|
@ -7,13 +7,15 @@ namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
public interface IWebsocket: IDisposable
|
||||
{
|
||||
void SetEnabledSslProtocols(SslProtocols protocols);
|
||||
|
||||
event Action OnClose;
|
||||
event Action<string> OnMessage;
|
||||
event Action<Exception> OnError;
|
||||
event Action OnOpen;
|
||||
|
||||
int Id { get; }
|
||||
bool ShouldReconnect { get; set; }
|
||||
Func<byte[], string> DataInterpreter { get; set; }
|
||||
DateTime? DisconnectTime { get; set; }
|
||||
string Url { get; }
|
||||
WebSocketState SocketState { get; }
|
||||
bool IsClosed { get; }
|
||||
@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
bool PingConnection { get; set; }
|
||||
TimeSpan PingInterval { get; set; }
|
||||
|
||||
void SetEnabledSslProtocols(SslProtocols protocols);
|
||||
Task<bool> Connect();
|
||||
void Send(string data);
|
||||
Task Close();
|
||||
|
49
CryptoExchange.Net/Objects/ByteOrderComparer.cs
Normal file
49
CryptoExchange.Net/Objects/ByteOrderComparer.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
public class ByteOrderComparer : IComparer<byte[]>
|
||||
{
|
||||
public int Compare(byte[] x, byte[] y)
|
||||
{
|
||||
// Shortcuts: If both are null, they are the same.
|
||||
if (x == null && y == null) return 0;
|
||||
|
||||
// If one is null and the other isn't, then the
|
||||
// one that is null is "lesser".
|
||||
if (x == null && y != null) return -1;
|
||||
if (x != null && y == null) return 1;
|
||||
|
||||
// Both arrays are non-null. Find the shorter
|
||||
// of the two lengths.
|
||||
int bytesToCompare = Math.Min(x.Length, y.Length);
|
||||
|
||||
// Compare the bytes.
|
||||
for (int index = 0; index < bytesToCompare; ++index)
|
||||
{
|
||||
// The x and y bytes.
|
||||
byte xByte = x[index];
|
||||
byte yByte = y[index];
|
||||
|
||||
// Compare result.
|
||||
int compareResult = Comparer<byte>.Default.Compare(xByte, yByte);
|
||||
|
||||
// If not the same, then return the result of the
|
||||
// comparison of the bytes, as they were the same
|
||||
// up until now.
|
||||
if (compareResult != 0) return compareResult;
|
||||
|
||||
// They are the same, continue.
|
||||
}
|
||||
|
||||
// The first n bytes are the same. Compare lengths.
|
||||
// If the lengths are the same, the arrays
|
||||
// are the same.
|
||||
if (x.Length == y.Length) return 0;
|
||||
|
||||
// Compare lengths.
|
||||
return x.Length < y.Length ? -1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
13
CryptoExchange.Net/Objects/Constants.cs
Normal file
13
CryptoExchange.Net/Objects/Constants.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
public class Constants
|
||||
{
|
||||
public const string GetMethod = "GET";
|
||||
public const string PostMethod = "POST";
|
||||
public const string DeleteMethod = "DELETE";
|
||||
public const string PutMethod = "PUT";
|
||||
|
||||
public const string JsonContentHeader = "application/json";
|
||||
public const string FormContentHeader = "application/x-www-form-urlencoded";
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
@ -36,7 +37,10 @@ namespace CryptoExchange.Net.Objects
|
||||
/// The log writers
|
||||
/// </summary>
|
||||
public List<TextWriter> LogWriters { get; set; } = new List<TextWriter>() {new DebugTextWriter()};
|
||||
}
|
||||
|
||||
public class ClientOptions: ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of ratelimiters to use
|
||||
/// </summary>
|
||||
@ -47,4 +51,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
}
|
||||
|
||||
public class SocketClientOptions: ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(2);
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,9 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
@ -18,23 +16,18 @@ using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
public abstract class ExchangeClient : IDisposable
|
||||
public abstract class RestClient: BaseClient
|
||||
{
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
protected string baseAddress;
|
||||
protected Log log;
|
||||
protected ApiProxy apiProxy;
|
||||
protected RateLimitingBehaviour rateLimitBehaviour;
|
||||
|
||||
protected PostParameters postParametersPosition = PostParameters.InBody;
|
||||
protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
protected AuthenticationProvider authProvider;
|
||||
private List<IRateLimiter> rateLimiters;
|
||||
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings()
|
||||
@ -42,10 +35,8 @@ namespace CryptoExchange.Net
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
||||
});
|
||||
|
||||
protected ExchangeClient(ExchangeOptions exchangeOptions, AuthenticationProvider authenticationProvider)
|
||||
protected RestClient(ClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider)
|
||||
{
|
||||
log = new Log();
|
||||
authProvider = authenticationProvider;
|
||||
Configure(exchangeOptions);
|
||||
}
|
||||
|
||||
@ -53,16 +44,8 @@ namespace CryptoExchange.Net
|
||||
/// Configure the client using the provided options
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions">Options</param>
|
||||
protected void Configure(ExchangeOptions exchangeOptions)
|
||||
protected void Configure(ClientOptions exchangeOptions)
|
||||
{
|
||||
log.UpdateWriters(exchangeOptions.LogWriters);
|
||||
log.Level = exchangeOptions.LogVerbosity;
|
||||
|
||||
baseAddress = exchangeOptions.BaseAddress;
|
||||
apiProxy = exchangeOptions.Proxy;
|
||||
if (apiProxy != null)
|
||||
log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}");
|
||||
|
||||
rateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
|
||||
rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in exchangeOptions.RateLimiters)
|
||||
@ -86,16 +69,6 @@ namespace CryptoExchange.Net
|
||||
rateLimiters.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the authentication provider
|
||||
/// </summary>
|
||||
/// <param name="authentictationProvider"></param>
|
||||
protected void SetAuthenticationProvider(AuthenticationProvider authentictationProvider)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Setting api credentials");
|
||||
authProvider = authentictationProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
@ -130,7 +103,7 @@ namespace CryptoExchange.Net
|
||||
return new CallResult<long>(0, new CantConnectError() { Message = "Ping failed: " + reply.Status });
|
||||
}
|
||||
|
||||
protected virtual async Task<CallResult<T>> ExecuteRequest<T>(Uri uri, string method = "GET", Dictionary<string, object> parameters = null, bool signed = false) where T : class
|
||||
protected virtual async Task<CallResult<T>> ExecuteRequest<T>(Uri uri, string method = Constants.GetMethod, Dictionary<string, object> parameters = null, bool signed = false) where T : class
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, $"Creating request for " + uri);
|
||||
if (signed && authProvider == null)
|
||||
@ -185,12 +158,12 @@ namespace CryptoExchange.Net
|
||||
if(authProvider != null)
|
||||
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed);
|
||||
|
||||
if((method == "GET" || method == "DELETE" || ((method == "POST" || method == "PUT") && postParametersPosition == PostParameters.InUri)) && parameters?.Any() == true)
|
||||
uriString += parameters.CreateParamString();
|
||||
if((method == Constants.GetMethod || method == Constants.DeleteMethod || (postParametersPosition == PostParameters.InUri)) && parameters?.Any() == true)
|
||||
uriString += "?" + parameters.CreateParamString(true);
|
||||
|
||||
var request = RequestFactory.Create(uriString);
|
||||
request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? "application/json": "application/x-www-form-urlencoded";
|
||||
request.Accept = "application/json";
|
||||
request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
request.Method = method;
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
@ -200,7 +173,7 @@ namespace CryptoExchange.Net
|
||||
foreach (var header in headers)
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
|
||||
if ((method == "POST" || method == "PUT") && postParametersPosition != PostParameters.InUri)
|
||||
if ((method == Constants.PostMethod || method == Constants.PutMethod) && postParametersPosition != PostParameters.InUri)
|
||||
{
|
||||
if(parameters?.Any() == true)
|
||||
WriteParamBody(request, parameters);
|
||||
@ -289,164 +262,5 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
return new ServerError(error);
|
||||
}
|
||||
|
||||
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer serializer = null) where T : class
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
var obj = JToken.Parse(data);
|
||||
if (checkObject && log.Level == LogVerbosity.Debug)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (obj is JObject o)
|
||||
{
|
||||
CheckObject(typeof(T), o);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ary = (JArray)obj;
|
||||
if (ary.HasValues && ary[0] is JObject jObject)
|
||||
CheckObject(typeof(T).GetElementType(), jObject);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Failed to check response data: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckObject(Type type, JObject obj)
|
||||
{
|
||||
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
|
||||
// If type has a custom JsonConverter we assume this will handle property mapping
|
||||
return;
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
return;
|
||||
|
||||
if (!obj.HasValues && type != typeof(object))
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, $"Expected `{type.Name}`, but received object was empty");
|
||||
return;
|
||||
}
|
||||
|
||||
bool isDif = false;
|
||||
var properties = new List<string>();
|
||||
var props = type.GetProperties();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
|
||||
if (ignore != null)
|
||||
continue;
|
||||
|
||||
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName);
|
||||
}
|
||||
foreach (var token in obj)
|
||||
{
|
||||
var d = properties.SingleOrDefault(p => p == token.Key);
|
||||
if (d == null)
|
||||
{
|
||||
d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower());
|
||||
if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
|
||||
isDif = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
properties.Remove(d);
|
||||
|
||||
var propType = GetProperty(d, props)?.PropertyType;
|
||||
if (propType == null)
|
||||
continue;
|
||||
if (!IsSimple(propType) && propType != typeof(DateTime))
|
||||
{
|
||||
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
|
||||
CheckObject(propType.GetElementType(), (JObject)token.Value[0]);
|
||||
else if (token.Value is JObject)
|
||||
CheckObject(propType, (JObject)token.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var propInfo = props.First(p => p.Name == prop ||
|
||||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
|
||||
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
|
||||
if (optional != null)
|
||||
continue;
|
||||
|
||||
isDif = true;
|
||||
log.Write(LogVerbosity.Warning, $"Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
|
||||
}
|
||||
|
||||
if (isDif)
|
||||
log.Write(LogVerbosity.Debug, "Returned data: " + obj);
|
||||
}
|
||||
|
||||
private PropertyInfo GetProperty(string name, PropertyInfo[] props)
|
||||
{
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
if (attr == null)
|
||||
{
|
||||
if (prop.Name.ToLower() == name.ToLower())
|
||||
return prop;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (((JsonPropertyAttribute)attr).PropertyName == name)
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
authProvider?.Credentials?.Dispose();
|
||||
log.Write(LogVerbosity.Debug, "Disposing exchange client");
|
||||
}
|
||||
}
|
||||
}
|
180
CryptoExchange.Net/SocketClient.cs
Normal file
180
CryptoExchange.Net/SocketClient.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
public abstract class SocketClient: BaseClient
|
||||
{
|
||||
#region fields
|
||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||
|
||||
private const SslProtocols protocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
|
||||
|
||||
protected List<SocketSubscription> sockets = new List<SocketSubscription>();
|
||||
|
||||
protected TimeSpan reconnectInterval;
|
||||
protected Func<byte[], string> dataInterpreter;
|
||||
#endregion
|
||||
|
||||
protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider)
|
||||
{
|
||||
Configure(exchangeOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the client using the provided options
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions">Options</param>
|
||||
protected void Configure(SocketClientOptions exchangeOptions)
|
||||
{
|
||||
reconnectInterval = exchangeOptions.ReconnectInterval;
|
||||
}
|
||||
|
||||
protected void SetDataInterpreter(Func<byte[], string> handler)
|
||||
{
|
||||
dataInterpreter = handler;
|
||||
}
|
||||
|
||||
protected virtual IWebsocket CreateSocket(string address)
|
||||
{
|
||||
var socket = SocketFactory.CreateWebsocket(log, address);
|
||||
log.Write(LogVerbosity.Debug, "Created new socket for " + address);
|
||||
|
||||
if (apiProxy != null)
|
||||
socket.SetProxy(apiProxy.Host, apiProxy.Port);
|
||||
|
||||
socket.SetEnabledSslProtocols(protocols);
|
||||
socket.DataInterpreter = dataInterpreter;
|
||||
socket.OnClose += () =>
|
||||
{
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
SocketOnClose(socket);
|
||||
};
|
||||
socket.OnError += (e) =>
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, $"Socket {socket.Id} error: " + e.ToString());
|
||||
SocketError(socket, e);
|
||||
};
|
||||
socket.OnOpen += () =>
|
||||
{
|
||||
socket.ShouldReconnect = true;
|
||||
SocketOpened(socket);
|
||||
};
|
||||
return socket;
|
||||
}
|
||||
|
||||
protected abstract void SocketOpened(IWebsocket socket);
|
||||
protected abstract void SocketClosed(IWebsocket socket);
|
||||
protected abstract void SocketError(IWebsocket socket, Exception ex);
|
||||
protected abstract bool SocketReconnect(SocketSubscription socket, TimeSpan disconnectedTime);
|
||||
|
||||
protected virtual CallResult<SocketSubscription> ConnectSocket(IWebsocket socket)
|
||||
{
|
||||
if (socket.Connect().Result)
|
||||
{
|
||||
var subscription = new SocketSubscription(socket);
|
||||
lock (sockets)
|
||||
sockets.Add(subscription);
|
||||
return new CallResult<SocketSubscription>(subscription, null);
|
||||
}
|
||||
|
||||
socket.Dispose();
|
||||
return new CallResult<SocketSubscription>(null, new CantConnectError());
|
||||
}
|
||||
|
||||
protected virtual void SocketOnClose(IWebsocket socket)
|
||||
{
|
||||
if (socket.ShouldReconnect)
|
||||
{
|
||||
log.Write(LogVerbosity.Info, $"Socket {socket.Id} Connection lost, going to try to reconnect");
|
||||
Task.Run(() =>
|
||||
{
|
||||
Thread.Sleep(reconnectInterval);
|
||||
if (!socket.Connect().Result)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, $"Socket {socket.Id} failed to reconnect");
|
||||
return; // Connect() should result in a SocketClosed event so we end up here again
|
||||
}
|
||||
|
||||
log.Write(LogVerbosity.Info, $"Socket {socket.Id} reconnected after {DateTime.UtcNow - socket.DisconnectTime.Value}");
|
||||
|
||||
SocketSubscription subscription;
|
||||
lock(sockets)
|
||||
subscription = sockets.Single(s => s.Socket == socket);
|
||||
|
||||
SocketReconnected(subscription, DateTime.UtcNow - socket.DisconnectTime.Value);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
socket.Dispose();
|
||||
lock (sockets)
|
||||
{
|
||||
var subscription = sockets.SingleOrDefault(s => s.Socket == socket);
|
||||
if(subscription != null)
|
||||
sockets.Remove(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Send<T>(IWebsocket socket, T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
||||
{
|
||||
Send(socket, JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
||||
}
|
||||
|
||||
protected virtual void Send(IWebsocket socket, string data)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, $"Socket {socket.Id} sending data: {data}");
|
||||
socket.Send(data);
|
||||
}
|
||||
|
||||
protected virtual async Task<CallResult<string>> SendAndWait<T>(IWebsocket socket, T obj, Func<JToken, bool> waitingFor, int timeout=5000)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var data = JsonConvert.SerializeObject(obj);
|
||||
ManualResetEvent evnt = new ManualResetEvent(false);
|
||||
string result = null;
|
||||
var onMessageAction = new Action<string>((msg) =>
|
||||
{
|
||||
if (!waitingFor(JToken.Parse(msg)))
|
||||
return;
|
||||
|
||||
log.Write(LogVerbosity.Debug, "Socket received query response: " + msg);
|
||||
result = msg;
|
||||
evnt?.Set();
|
||||
});
|
||||
|
||||
socket.OnMessage += onMessageAction;
|
||||
Send(socket, data);
|
||||
evnt.WaitOne(timeout);
|
||||
socket.OnMessage -= onMessageAction;
|
||||
evnt.Dispose();
|
||||
evnt = null;
|
||||
if (result == null)
|
||||
return new CallResult<string>(null, new ServerError("No response from server"));
|
||||
return new CallResult<string>(result, null);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
lock(sockets)
|
||||
foreach (var socket in sockets)
|
||||
socket.Socket.Dispose();
|
||||
sockets.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -11,22 +11,30 @@ using SuperSocket.ClientEngine;
|
||||
using SuperSocket.ClientEngine.Proxy;
|
||||
using WebSocket4Net;
|
||||
|
||||
namespace CryptoExchange.Net.Implementation
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
public class BaseSocket: IWebsocket
|
||||
{
|
||||
internal static int lastStreamId;
|
||||
private static readonly object streamIdLock = new object();
|
||||
|
||||
protected WebSocket socket;
|
||||
protected Log log;
|
||||
protected object socketLock = new object();
|
||||
protected DateTime? lostTime = null;
|
||||
|
||||
protected readonly List<Action<Exception>> errorhandlers = new List<Action<Exception>>();
|
||||
protected readonly List<Action> openhandlers = new List<Action>();
|
||||
protected readonly List<Action> closehandlers = new List<Action>();
|
||||
protected readonly List<Action<string>> messagehandlers = new List<Action<string>>();
|
||||
protected readonly List<Action<Exception>> errorHandlers = new List<Action<Exception>>();
|
||||
protected readonly List<Action> openHandlers = new List<Action>();
|
||||
protected readonly List<Action> closeHandlers = new List<Action>();
|
||||
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
|
||||
|
||||
public int Id { get; }
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
public string Url { get; }
|
||||
public bool IsClosed => socket.State == WebSocketState.Closed;
|
||||
public bool IsOpen => socket.State == WebSocketState.Open;
|
||||
public Func<byte[], string> DataInterpreter { get; set; }
|
||||
|
||||
public bool PingConnection
|
||||
{
|
||||
@ -56,6 +64,7 @@ namespace CryptoExchange.Net.Implementation
|
||||
|
||||
public BaseSocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
||||
{
|
||||
Id = NextStreamId();
|
||||
this.log = log;
|
||||
Url = url;
|
||||
socket = new WebSocket(url, cookies: cookies.ToList(), customHeaderItems: headers.ToList())
|
||||
@ -63,33 +72,38 @@ namespace CryptoExchange.Net.Implementation
|
||||
EnableAutoSendPing = true,
|
||||
AutoSendPingInterval = 10
|
||||
};
|
||||
socket.Opened += (o, s) => Handle(openhandlers);
|
||||
socket.Closed += (o, s) => Handle(closehandlers);
|
||||
socket.Error += (o, s) => Handle(errorhandlers, s.Exception);
|
||||
socket.MessageReceived += (o, s) => Handle(messagehandlers, s.Message);
|
||||
socket.EnableAutoSendPing = true;
|
||||
socket.AutoSendPingInterval = 10;
|
||||
socket.Opened += (o, s) => Handle(openHandlers);
|
||||
socket.Closed += (o, s) => Handle(closeHandlers);
|
||||
socket.Error += (o, s) => Handle(errorHandlers, s.Exception);
|
||||
socket.MessageReceived += (o, s) => Handle(messageHandlers, s.Message);
|
||||
socket.DataReceived += (o, s) => HandleByteData(s.Data);
|
||||
}
|
||||
|
||||
private void HandleByteData(byte[] data)
|
||||
{
|
||||
var message = DataInterpreter(data);
|
||||
Handle(messageHandlers, message);
|
||||
}
|
||||
|
||||
public event Action OnClose
|
||||
{
|
||||
add => closehandlers.Add(value);
|
||||
remove => closehandlers.Remove(value);
|
||||
add => closeHandlers.Add(value);
|
||||
remove => closeHandlers.Remove(value);
|
||||
}
|
||||
public event Action<string> OnMessage
|
||||
{
|
||||
add => messagehandlers.Add(value);
|
||||
remove => messagehandlers.Remove(value);
|
||||
add => messageHandlers.Add(value);
|
||||
remove => messageHandlers.Remove(value);
|
||||
}
|
||||
public event Action<Exception> OnError
|
||||
{
|
||||
add => errorhandlers.Add(value);
|
||||
remove => errorhandlers.Remove(value);
|
||||
add => errorHandlers.Add(value);
|
||||
remove => errorHandlers.Remove(value);
|
||||
}
|
||||
public event Action OnOpen
|
||||
{
|
||||
add => openhandlers.Add(value);
|
||||
remove => openhandlers.Remove(value);
|
||||
add => openHandlers.Add(value);
|
||||
remove => openHandlers.Remove(value);
|
||||
}
|
||||
|
||||
protected static void Handle(List<Action> handlers)
|
||||
@ -112,12 +126,12 @@ namespace CryptoExchange.Net.Implementation
|
||||
{
|
||||
if (socket == null || IsClosed)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Socket was already closed/disposed");
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} was already closed/disposed");
|
||||
return;
|
||||
}
|
||||
|
||||
var waitLock = new object();
|
||||
log.Write(LogVerbosity.Debug, "Closing websocket");
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} closing");
|
||||
ManualResetEvent evnt = new ManualResetEvent(false);
|
||||
var handler = new EventHandler((o, a) =>
|
||||
{
|
||||
@ -133,7 +147,7 @@ namespace CryptoExchange.Net.Implementation
|
||||
evnt.Dispose();
|
||||
evnt = null;
|
||||
}
|
||||
log.Write(LogVerbosity.Debug, "Websocket closed");
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} closed");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
@ -150,7 +164,7 @@ namespace CryptoExchange.Net.Implementation
|
||||
bool connected;
|
||||
lock (socketLock)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Connecting websocket");
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} connecting");
|
||||
var waitLock = new object();
|
||||
ManualResetEvent evnt = new ManualResetEvent(false);
|
||||
var handler = new EventHandler((o, a) =>
|
||||
@ -178,9 +192,9 @@ namespace CryptoExchange.Net.Implementation
|
||||
}
|
||||
connected = socket.State == WebSocketState.Open;
|
||||
if (connected)
|
||||
log.Write(LogVerbosity.Debug, "Websocket connected");
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} connected");
|
||||
else
|
||||
log.Write(LogVerbosity.Debug, "Websocket connection failed, state: " + socket.State);
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State);
|
||||
}
|
||||
|
||||
if (socket.State == WebSocketState.Connecting)
|
||||
@ -208,15 +222,24 @@ namespace CryptoExchange.Net.Implementation
|
||||
lock (socketLock)
|
||||
{
|
||||
if (socket != null)
|
||||
log.Write(LogVerbosity.Debug, "Disposing websocket");
|
||||
log.Write(LogVerbosity.Debug, $"Socket {Id} sisposing websocket");
|
||||
|
||||
socket?.Dispose();
|
||||
socket = null;
|
||||
|
||||
errorhandlers.Clear();
|
||||
openhandlers.Clear();
|
||||
closehandlers.Clear();
|
||||
messagehandlers.Clear();
|
||||
errorHandlers.Clear();
|
||||
openHandlers.Clear();
|
||||
closeHandlers.Clear();
|
||||
messageHandlers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private int NextStreamId()
|
||||
{
|
||||
lock (streamIdLock)
|
||||
{
|
||||
lastStreamId++;
|
||||
return lastStreamId;
|
||||
}
|
||||
}
|
||||
}
|
39
CryptoExchange.Net/Sockets/SocketSubscription.cs
Normal file
39
CryptoExchange.Net/Sockets/SocketSubscription.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
public class SocketSubscription
|
||||
{
|
||||
public event Action ConnectionLost;
|
||||
public event Action ConnectionRestored;
|
||||
|
||||
public IWebsocket Socket { get; set; }
|
||||
public object Request { get; set; }
|
||||
|
||||
private bool lostTriggered;
|
||||
|
||||
public SocketSubscription(IWebsocket socket)
|
||||
{
|
||||
Socket = socket;
|
||||
|
||||
Socket.OnClose += () =>
|
||||
{
|
||||
if (lostTriggered)
|
||||
return;
|
||||
|
||||
lostTriggered = true;
|
||||
if (Socket.ShouldReconnect)
|
||||
ConnectionLost?.Invoke();
|
||||
};
|
||||
Socket.OnOpen += () =>
|
||||
{
|
||||
lostTriggered = false;
|
||||
if (Socket.DisconnectTime != null)
|
||||
ConnectionRestored?.Invoke();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Implementation
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
public class WebsocketFactory : IWebsocketFactory
|
||||
{
|
Loading…
x
Reference in New Issue
Block a user