1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-10 01:16:24 +00:00
This commit is contained in:
Jan Korf 2018-08-12 13:36:27 +02:00
commit e035a34316
21 changed files with 530 additions and 87 deletions

View File

@ -173,12 +173,15 @@ namespace CryptoExchange.Net.UnitTests
TestImplementation client; TestImplementation client;
if (withOptions) if (withOptions)
{ {
client = new TestImplementation(new ExchangeOptions() var options = new ExchangeOptions()
{ {
ApiCredentials = new ApiCredentials("Test", "Test2"), ApiCredentials = new ApiCredentials("Test", "Test2"),
LogVerbosity = verbosity, LogVerbosity = verbosity
LogWriters = new List<TextWriter>() { tw } };
}); if (tw != null)
options.LogWriters = new List<TextWriter>() { tw };
client = new TestImplementation(options);
} }
else else
{ {

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Attributes
{
public class JsonOptionalPropertyAttribute : Attribute
{
public JsonOptionalPropertyAttribute() { }
}
}

View File

@ -1,27 +1,88 @@
using System; using System;
using System.Security;
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication
{ {
public class ApiCredentials public class ApiCredentials: IDisposable
{ {
/// <summary> /// <summary>
/// The api key /// The api key to authenticate requests
/// </summary> /// </summary>
public string Key { get; } public SecureString Key { get; }
/// <summary> /// <summary>
/// The api secret /// The api secret to authenticate requests
/// </summary> /// </summary>
public string Secret { get; } public SecureString Secret { get; }
public ApiCredentials() { } /// <summary>
/// The private key to authenticate requests
/// </summary>
public SecureString PrivateKey { get; }
public ApiCredentials(string key, string secret) /// <summary>
/// Create Api credentials providing a private key for authenication
/// </summary>
/// <param name="privateKey">The private key used for signing</param>
public ApiCredentials(SecureString privateKey)
{ {
if(string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) PrivateKey = privateKey;
throw new ArgumentException("Apikey or apisecret not provided"); }
/// <summary>
/// Create Api credentials providing a private key for authenication
/// </summary>
/// <param name="privateKey">The private key used for signing</param>
public ApiCredentials(string privateKey)
{
if(string.IsNullOrEmpty(privateKey))
throw new ArgumentException("Private key can't be null/empty");
var securePrivateKey = new SecureString();
foreach (var c in privateKey)
securePrivateKey.AppendChar(c);
securePrivateKey.MakeReadOnly();
PrivateKey = securePrivateKey;
}
/// <summary>
/// Create Api credentials providing a api key and secret for authenciation
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(SecureString key, SecureString secret)
{
Key = key; Key = key;
Secret = secret; Secret = secret;
} }
/// <summary>
/// Create Api credentials providing a private key for authenication
/// </summary>
/// <param name="privateKey">The private key used for signing</param>
public ApiCredentials(string key, string secret)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
var secureApiKey = new SecureString();
foreach (var c in key)
secureApiKey.AppendChar(c);
secureApiKey.MakeReadOnly();
Key = secureApiKey;
var secureApiSecret = new SecureString();
foreach (var c in secret)
secureApiSecret.AppendChar(c);
secureApiSecret.MakeReadOnly();
Secret = secureApiSecret;
}
public void Dispose()
{
Key?.Dispose();
Secret?.Dispose();
PrivateKey?.Dispose();
}
} }
} }

View File

@ -11,9 +11,25 @@ namespace CryptoExchange.Net.Authentication
Credentials = credentials; Credentials = credentials;
} }
public abstract string AddAuthenticationToUriString(string uri, bool signed); public virtual string AddAuthenticationToUriString(string uri, bool signed)
public abstract IRequest AddAuthenticationToRequest(IRequest request, bool signed); {
public abstract string Sign(string toSign); return uri;
}
public virtual IRequest AddAuthenticationToRequest(IRequest request, bool signed)
{
return request;
}
public virtual string Sign(string toSign)
{
return toSign;
}
public virtual byte[] Sign(byte[] toSign)
{
return toSign;
}
protected string ByteToString(byte[] buff) protected string ByteToString(byte[] buff)
{ {

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Globalization;
using System.Linq;
using System.Reflection; using System.Reflection;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -41,15 +43,37 @@ namespace CryptoExchange.Net.Converters
if (((JToken)value).Type == JTokenType.Null) if (((JToken)value).Type == JTokenType.Null)
value = null; value = null;
if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?))
&& value.ToString().Contains("e"))
{
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
property.SetValue(result, dec);
}
else
{
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType)); property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
} }
} }
}
return result; return result;
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{ {
throw new NotImplementedException(); writer.WriteStartArray();
var props = value.GetType().GetProperties();
var ordered = props.OrderBy(p => p.GetCustomAttribute<ArrayPropertyAttribute>()?.Index);
foreach (var prop in ordered)
{
var converterAttribute = (JsonConverterAttribute)prop.GetCustomAttribute(typeof(JsonConverterAttribute));
if(converterAttribute != null)
writer.WriteValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
else
writer.WriteValue(JsonConvert.SerializeObject(prop.GetValue(value)));
}
writer.WriteEndArray();
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -25,20 +26,30 @@ namespace CryptoExchange.Net.Converters
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{ {
var val = Mapping.SingleOrDefault(v => v.Value == reader.Value.ToString()).Key; if (reader.Value == null)
if (val != null) return null;
return val;
return Mapping.Single(v => v.Value.ToLower() == reader.Value.ToString().ToLower()).Key; var value = reader.Value.ToString();
if (Mapping.ContainsValue(value))
return Mapping.Single(m => m.Value == value).Key;
var lowerResult = Mapping.SingleOrDefault(m => m.Value.ToLower() == value.ToLower());
if (!lowerResult.Equals(default(KeyValuePair<T, string>)))
return lowerResult.Key;
Debug.WriteLine($"Cannot map enum. Type: {typeof(T)}, Value: {value}");
return null;
} }
public T ReadString(string data) public T ReadString(string data)
{ {
return Mapping.Single(v => v.Value == data).Key; return Mapping.SingleOrDefault(v => v.Value == data).Key;
} }
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ {
return objectType == typeof(T); // Check if it is type, or nullable of type
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
} }
} }
} }

View File

@ -12,6 +12,9 @@ namespace CryptoExchange.Net.Converters
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.Value == null)
return null;
var t = long.Parse(reader.Value.ToString()); var t = long.Parse(reader.Value.ToString());
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t); return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Globalization;
using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters namespace CryptoExchange.Net.Converters
@ -14,8 +13,11 @@ namespace CryptoExchange.Net.Converters
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{ {
var t = Convert.ToInt64(Math.Round(double.Parse(reader.Value.ToString()) * 1000)); if (reader.Value.GetType() == typeof(double))
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t); return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds((double)reader.Value);
var t = double.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture);
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(t);
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)

View File

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

View File

@ -7,7 +7,7 @@
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<PackageVersion>0.0.18</PackageVersion> <PackageVersion>0.0.36</PackageVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE</PackageLicenseUrl>

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging; using CryptoExchange.Net.Logging;
@ -16,7 +17,7 @@ using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
public abstract class ExchangeClient: IDisposable public abstract class ExchangeClient : IDisposable
{ {
public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
@ -27,6 +28,11 @@ namespace CryptoExchange.Net
protected AuthenticationProvider authProvider; protected AuthenticationProvider authProvider;
private List<IRateLimiter> rateLimiters; private List<IRateLimiter> rateLimiters;
private static JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings()
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc
});
protected ExchangeClient(ExchangeOptions exchangeOptions, AuthenticationProvider authenticationProvider) protected ExchangeClient(ExchangeOptions exchangeOptions, AuthenticationProvider authenticationProvider)
{ {
log = new Log(); log = new Log();
@ -44,7 +50,7 @@ namespace CryptoExchange.Net
log.Level = exchangeOptions.LogVerbosity; log.Level = exchangeOptions.LogVerbosity;
apiProxy = exchangeOptions.Proxy; apiProxy = exchangeOptions.Proxy;
if(apiProxy != null) if (apiProxy != null)
log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}"); log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}");
rateLimitBehaviour = exchangeOptions.RateLimitingBehaviour; rateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
@ -82,6 +88,7 @@ namespace CryptoExchange.Net
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 = "GET", Dictionary<string, object> parameters = null, bool signed = false) where T : class
{ {
log.Write(LogVerbosity.Debug, $"Creating request for " + uri);
if (signed && authProvider == null) if (signed && authProvider == null)
{ {
log.Write(LogVerbosity.Warning, $"Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); log.Write(LogVerbosity.Warning, $"Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
@ -91,7 +98,10 @@ namespace CryptoExchange.Net
var request = ConstructRequest(uri, method, parameters, signed); var request = ConstructRequest(uri, method, parameters, signed);
if (apiProxy != null) if (apiProxy != null)
{
log.Write(LogVerbosity.Debug, "Setting proxy");
request.SetProxy(apiProxy.Host, apiProxy.Port); request.SetProxy(apiProxy.Host, apiProxy.Port);
}
foreach (var limiter in rateLimiters) foreach (var limiter in rateLimiters)
{ {
@ -105,7 +115,18 @@ namespace CryptoExchange.Net
log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}");
} }
log.Write(LogVerbosity.Debug, $"Sending {(signed ? "signed": "")} request to {request.Uri}"); string paramString = null;
if (parameters != null)
{
paramString = "with parameters";
foreach (var param in parameters)
paramString += $" {param.Key}={(param.Value.GetType().IsArray ? $"[{string.Join(", ", ((object[])param.Value).Select(p => p.ToString()))}]": param.Value )},";
paramString = paramString.Trim(',');
}
log.Write(LogVerbosity.Debug, $"Sending {(signed ? "signed" : "")} request to {request.Uri} {(paramString ?? "")}");
var result = await ExecuteRequest(request).ConfigureAwait(false); var result = await ExecuteRequest(request).ConfigureAwait(false);
return result.Error != null ? new CallResult<T>(null, result.Error) : Deserialize<T>(result.Data); return result.Error != null ? new CallResult<T>(null, result.Error) : Deserialize<T>(result.Data);
} }
@ -119,7 +140,14 @@ namespace CryptoExchange.Net
if (!uriString.EndsWith("?")) if (!uriString.EndsWith("?"))
uriString += "?"; uriString += "?";
uriString += $"{string.Join("&", parameters.Select(s => $"{s.Key}={s.Value}"))}"; 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("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={s.Value}"))}";
uriString = uriString.TrimEnd('&');
} }
if (authProvider != null) if (authProvider != null)
@ -185,8 +213,11 @@ namespace CryptoExchange.Net
return new ServerError(error); return new ServerError(error);
} }
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true) where T: class protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer serializer = null) where T : class
{ {
if (serializer == null)
serializer = defaultSerializer;
try try
{ {
var obj = JToken.Parse(data); var obj = JToken.Parse(data);
@ -195,10 +226,12 @@ namespace CryptoExchange.Net
try try
{ {
if (obj is JObject o) if (obj is JObject o)
{
CheckObject(typeof(T), o); CheckObject(typeof(T), o);
}
else else
{ {
var ary = (JArray) obj; var ary = (JArray)obj;
if (ary.HasValues && ary[0] is JObject jObject) if (ary.HasValues && ary[0] is JObject jObject)
CheckObject(typeof(T).GetElementType(), jObject); CheckObject(typeof(T).GetElementType(), jObject);
} }
@ -209,17 +242,23 @@ namespace CryptoExchange.Net
} }
} }
return new CallResult<T>(obj.ToObject<T>(), null); return new CallResult<T>(obj.ToObject<T>(serializer), null);
} }
catch (JsonReaderException jre) catch (JsonReaderException jre)
{ {
var info = $"{jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {data}"; var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {data}";
log.Write(LogVerbosity.Error, info); log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info)); return new CallResult<T>(null, new DeserializeError(info));
} }
catch (JsonSerializationException jse) catch (JsonSerializationException jse)
{ {
var info = $"{jse.Message}. Received data: {data}"; 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); log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info)); return new CallResult<T>(null, new DeserializeError(info));
} }
@ -227,6 +266,19 @@ namespace CryptoExchange.Net
private void CheckObject(Type type, JObject obj) 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; bool isDif = false;
var properties = new List<string>(); var properties = new List<string>();
var props = type.GetProperties(); var props = type.GetProperties();
@ -237,7 +289,7 @@ namespace CryptoExchange.Net
if (ignore != null) if (ignore != null)
continue; continue;
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute) attr).PropertyName); properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName);
} }
foreach (var token in obj) foreach (var token in obj)
{ {
@ -247,7 +299,7 @@ namespace CryptoExchange.Net
d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower()); d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower());
if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
{ {
log.Write(LogVerbosity.Warning, $"Didn't find property `{token.Key}` in object of type `{type.Name}`"); log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
isDif = true; isDif = true;
continue; continue;
} }
@ -259,20 +311,26 @@ namespace CryptoExchange.Net
continue; continue;
if (!IsSimple(propType) && propType != typeof(DateTime)) if (!IsSimple(propType) && propType != typeof(DateTime))
{ {
if(propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject) if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
CheckObject(propType.GetElementType(), (JObject)token.Value[0]); CheckObject(propType.GetElementType(), (JObject)token.Value[0]);
else if(token.Value is JObject) else if (token.Value is JObject)
CheckObject(propType, (JObject)token.Value); CheckObject(propType, (JObject)token.Value);
} }
} }
foreach (var prop in properties) foreach (var prop in properties)
{ {
var propInfo = props.FirstOrDefault(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; isDif = true;
log.Write(LogVerbosity.Warning, $"Didn't find key `{prop}` in returned data object of type `{type.Name}`"); log.Write(LogVerbosity.Warning, $"Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
} }
if(isDif) if (isDif)
log.Write(LogVerbosity.Debug, "Returned data: " + obj); log.Write(LogVerbosity.Debug, "Returned data: " + obj);
} }
@ -288,7 +346,7 @@ namespace CryptoExchange.Net
} }
else else
{ {
if (((JsonPropertyAttribute) attr).PropertyName == name) if (((JsonPropertyAttribute)attr).PropertyName == name)
return prop; return prop;
} }
} }
@ -310,6 +368,7 @@ namespace CryptoExchange.Net
public virtual void Dispose() public virtual void Dispose()
{ {
authProvider?.Credentials?.Dispose();
log.Write(LogVerbosity.Debug, "Disposing exchange client"); log.Write(LogVerbosity.Debug, "Disposing exchange client");
} }
} }

View File

@ -1,4 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Security;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@ -25,5 +28,30 @@ namespace CryptoExchange.Net
if (value != null) if (value != null)
parameters.Add(key, value); parameters.Add(key, value);
} }
public static string GetString(this SecureString source)
{
string result = null;
int length = source.Length;
IntPtr pointer = IntPtr.Zero;
char[] chars = new char[length];
try
{
pointer = Marshal.SecureStringToBSTR(source);
Marshal.Copy(pointer, chars, 0, length);
result = string.Join("", chars);
}
finally
{
if (pointer != IntPtr.Zero)
{
Marshal.ZeroFreeBSTR(pointer);
}
}
return result;
}
} }
} }

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Security.Authentication; using System.Security.Authentication;
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 SuperSocket.ClientEngine;
using SuperSocket.ClientEngine.Proxy; using SuperSocket.ClientEngine.Proxy;
using WebSocket4Net; using WebSocket4Net;
@ -14,6 +17,8 @@ namespace CryptoExchange.Net.Implementation
public class BaseSocket: IWebsocket public class BaseSocket: IWebsocket
{ {
protected WebSocket socket; protected WebSocket socket;
protected Log log;
protected object socketLock = new object();
protected readonly List<Action<Exception>> errorhandlers = new List<Action<Exception>>(); protected readonly List<Action<Exception>> errorhandlers = new List<Action<Exception>>();
protected readonly List<Action> openhandlers = new List<Action>(); protected readonly List<Action> openhandlers = new List<Action>();
@ -35,12 +40,13 @@ namespace CryptoExchange.Net.Implementation
set => socket.AutoSendPingInterval = (int) Math.Round(value.TotalSeconds); set => socket.AutoSendPingInterval = (int) Math.Round(value.TotalSeconds);
} }
public BaseSocket(string url):this(url, new Dictionary<string, string>(), new Dictionary<string, string>()) public BaseSocket(Log log, string url):this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>())
{ {
} }
public BaseSocket(string url, IDictionary<string, string> cookies, IDictionary<string, string> headers) public BaseSocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
{ {
this.log = log;
socket = new WebSocket(url, cookies: cookies.ToList(), customHeaderItems: headers.ToList()); socket = new WebSocket(url, cookies: cookies.ToList(), customHeaderItems: headers.ToList());
socket.EnableAutoSendPing = true; socket.EnableAutoSendPing = true;
socket.AutoSendPingInterval = 10; socket.AutoSendPingInterval = 10;
@ -70,7 +76,7 @@ namespace CryptoExchange.Net.Implementation
public event Action OnOpen public event Action OnOpen
{ {
add => openhandlers.Add(value); add => openhandlers.Add(value);
remove => closehandlers.Remove(value); remove => openhandlers.Remove(value);
} }
protected static void Handle(List<Action> handlers) protected static void Handle(List<Action> handlers)
@ -89,12 +95,23 @@ namespace CryptoExchange.Net.Implementation
{ {
await Task.Run(() => await Task.Run(() =>
{ {
lock (socketLock)
{
if (socket == null || IsClosed)
{
log.Write(LogVerbosity.Debug, "Socket was already closed/disposed");
return;
}
log.Write(LogVerbosity.Debug, "Closing websocket");
ManualResetEvent evnt = new ManualResetEvent(false); ManualResetEvent evnt = new ManualResetEvent(false);
var handler = new EventHandler((o, a) => evnt.Set()); var handler = new EventHandler((o, a) => evnt.Set());
socket.Closed += handler; socket.Closed += handler;
socket.Close(); socket.Close();
evnt.WaitOne(); bool triggered = evnt.WaitOne(3000);
socket.Closed -= handler; socket.Closed -= handler;
log.Write(LogVerbosity.Debug, "Websocket closed");
}
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
@ -107,15 +124,34 @@ namespace CryptoExchange.Net.Implementation
{ {
return await Task.Run(() => return await Task.Run(() =>
{ {
bool connected;
lock (socketLock)
{
log.Write(LogVerbosity.Debug, "Connecting websocket");
ManualResetEvent evnt = new ManualResetEvent(false); ManualResetEvent evnt = new ManualResetEvent(false);
var handler = new EventHandler((o, a) => evnt.Set()); var handler = new EventHandler((o, a) => evnt?.Set());
var errorHandler = new EventHandler<ErrorEventArgs>((o, a) => evnt?.Set());
socket.Opened += handler; socket.Opened += handler;
socket.Closed += handler; socket.Closed += handler;
socket.Error += errorHandler;
socket.Open(); socket.Open();
evnt.WaitOne(); evnt.WaitOne(TimeSpan.FromSeconds(15));
socket.Opened -= handler; socket.Opened -= handler;
socket.Closed -= handler; socket.Closed -= handler;
return socket.State == WebSocketState.Open; socket.Error -= errorHandler;
connected = socket.State == WebSocketState.Open;
if (connected)
log.Write(LogVerbosity.Debug, "Websocket connected");
else
log.Write(LogVerbosity.Debug, "Websocket connection failed, state: " + socket.State);
evnt.Dispose();
evnt = null;
}
if (socket.State == WebSocketState.Connecting)
Close().Wait();
return connected;
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
@ -134,7 +170,13 @@ namespace CryptoExchange.Net.Implementation
public void Dispose() public void Dispose()
{ {
lock (socketLock)
{
if (socket != null)
log.Write(LogVerbosity.Debug, "Disposing websocket");
socket?.Dispose(); socket?.Dispose();
socket = null;
errorhandlers.Clear(); errorhandlers.Clear();
openhandlers.Clear(); openhandlers.Clear();
@ -142,4 +184,5 @@ namespace CryptoExchange.Net.Implementation
messagehandlers.Clear(); messagehandlers.Clear();
} }
} }
}
} }

View File

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Implementation
{
public class TestWebsocket: IWebsocket
{
public List<string> MessagesSend = new List<string>();
public void Dispose()
{
}
public void SetEnabledSslProtocols(SslProtocols protocols)
{
}
public void SetProxy(string host, int port)
{
}
public event Action OnClose;
public event Action<string> OnMessage;
public event Action<Exception> OnError;
public event Action OnOpen;
public bool IsClosed { get; private set; } = true;
public bool IsOpen { get; private set; }
public bool PingConnection { get; set; }
public TimeSpan PingInterval { get; set; }
public bool HasConnection = true;
public Task<bool> Connect()
{
if (!HasConnection)
{
OnError(new Exception("No connection"));
return Task.FromResult(false);
}
IsClosed = false;
IsOpen = true;
OnOpen?.Invoke();
return Task.FromResult(true);
}
public void Send(string data)
{
if (!HasConnection)
{
OnError(new Exception("No connection"));
Close();
return;
}
MessagesSend.Add(data);
}
public async Task EnqueueMessage(string data, int wait)
{
await Task.Delay(wait);
OnMessage?.Invoke(data);
}
public async Task InvokeError(Exception ex, bool closeConnection)
{
await Task.Delay(10);
OnError?.Invoke(ex);
if (closeConnection)
await Close();
}
public Task Close()
{
IsClosed = true;
IsOpen = false;
OnClose?.Invoke();
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,13 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
namespace CryptoExchange.Net.Implementation
{
public class TestWebsocketFactory : IWebsocketFactory
{
public IWebsocket CreateWebsocket(Log log, string url)
{
return new TestWebsocket();
}
}
}

View File

@ -1,12 +1,13 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
namespace CryptoExchange.Net.Implementation namespace CryptoExchange.Net.Implementation
{ {
public class WebsocketFactory : IWebsocketFactory public class WebsocketFactory : IWebsocketFactory
{ {
public IWebsocket CreateWebsocket(string url) public IWebsocket CreateWebsocket(Log log, string url)
{ {
return new BaseSocket(url); return new BaseSocket(log, url);
} }
} }
} }

View File

@ -1,7 +1,9 @@
namespace CryptoExchange.Net.Interfaces using CryptoExchange.Net.Logging;
namespace CryptoExchange.Net.Interfaces
{ {
public interface IWebsocketFactory public interface IWebsocketFactory
{ {
IWebsocket CreateWebsocket(string url); IWebsocket CreateWebsocket(Log log, string url);
} }
} }

View File

@ -11,6 +11,7 @@ namespace CryptoExchange.Net.Logging
private List<TextWriter> writers; private List<TextWriter> writers;
private LogVerbosity level = LogVerbosity.Info; private LogVerbosity level = LogVerbosity.Info;
public LogVerbosity Level public LogVerbosity Level
{ {
get => level; get => level;
@ -36,16 +37,19 @@ namespace CryptoExchange.Net.Logging
public void Write(LogVerbosity logType, string message) public void Write(LogVerbosity logType, string message)
{ {
foreach (var writer in writers) if ((int)logType < (int)Level)
return;
string logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logType} | {message}";
foreach (var writer in writers.ToList())
{ {
try try
{ {
if ((int) logType >= (int) Level) writer.WriteLine(logMessage);
writer.WriteLine($"{DateTime.Now:yyyy/MM/dd hh:mm:ss:fff} | {logType} | {message}");
} }
catch (Exception e) catch (Exception e)
{ {
Debug.WriteLine("Failed to write log: " + e.Message); Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.Message);
} }
} }
} }

View File

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

View File

@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Requests
public async Task<Stream> GetRequestStream() public async Task<Stream> GetRequestStream()
{ {
return await request.GetRequestStreamAsync(); return await request.GetRequestStreamAsync().ConfigureAwait(false);
} }
public async Task<IResponse> GetResponse() public async Task<IResponse> GetResponse()