mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-09 17:06:19 +00:00
Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net
This commit is contained in:
commit
e035a34316
@ -173,12 +173,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TestImplementation client;
|
||||
if (withOptions)
|
||||
{
|
||||
client = new TestImplementation(new ExchangeOptions()
|
||||
var options = new ExchangeOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("Test", "Test2"),
|
||||
LogVerbosity = verbosity,
|
||||
LogWriters = new List<TextWriter>() { tw }
|
||||
});
|
||||
LogVerbosity = verbosity
|
||||
};
|
||||
if (tw != null)
|
||||
options.LogWriters = new List<TextWriter>() { tw };
|
||||
|
||||
client = new TestImplementation(options);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
public class JsonOptionalPropertyAttribute : Attribute
|
||||
{
|
||||
public JsonOptionalPropertyAttribute() { }
|
||||
}
|
||||
}
|
@ -1,27 +1,88 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
public class ApiCredentials
|
||||
public class ApiCredentials: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key
|
||||
/// The api key to authenticate requests
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
public SecureString Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret
|
||||
/// The api secret to authenticate requests
|
||||
/// </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))
|
||||
throw new ArgumentException("Apikey or apisecret not provided");
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,25 @@ namespace CryptoExchange.Net.Authentication
|
||||
Credentials = credentials;
|
||||
}
|
||||
|
||||
public abstract string AddAuthenticationToUriString(string uri, bool signed);
|
||||
public abstract IRequest AddAuthenticationToRequest(IRequest request, bool signed);
|
||||
public abstract string Sign(string toSign);
|
||||
public virtual string AddAuthenticationToUriString(string uri, bool signed)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -41,7 +43,17 @@ namespace CryptoExchange.Net.Converters
|
||||
if (((JToken)value).Type == JTokenType.Null)
|
||||
value = null;
|
||||
|
||||
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@ -49,7 +61,19 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -25,20 +26,30 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var val = Mapping.SingleOrDefault(v => v.Value == reader.Value.ToString()).Key;
|
||||
if (val != null)
|
||||
return val;
|
||||
return Mapping.Single(v => v.Value.ToLower() == reader.Value.ToString().ToLower()).Key;
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
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)
|
||||
{
|
||||
return Mapping.Single(v => v.Value == data).Key;
|
||||
return Mapping.SingleOrDefault(v => v.Value == data).Key;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,9 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var t = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
@ -14,8 +13,11 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var t = Convert.ToInt64(Math.Round(double.Parse(reader.Value.ToString()) * 1000));
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
|
||||
if (reader.Value.GetType() == typeof(double))
|
||||
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)
|
||||
|
32
CryptoExchange.Net/Converters/UTCDateTimeConverter.cs
Normal file
32
CryptoExchange.Net/Converters/UTCDateTimeConverter.cs
Normal 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?);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<PackageVersion>0.0.18</PackageVersion>
|
||||
<PackageVersion>0.0.36</PackageVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE</PackageLicenseUrl>
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
@ -16,7 +17,7 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
public abstract class ExchangeClient: IDisposable
|
||||
public abstract class ExchangeClient : IDisposable
|
||||
{
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
@ -26,7 +27,12 @@ namespace CryptoExchange.Net
|
||||
|
||||
protected AuthenticationProvider authProvider;
|
||||
private List<IRateLimiter> rateLimiters;
|
||||
|
||||
|
||||
private static JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings()
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
||||
});
|
||||
|
||||
protected ExchangeClient(ExchangeOptions exchangeOptions, AuthenticationProvider authenticationProvider)
|
||||
{
|
||||
log = new Log();
|
||||
@ -44,7 +50,7 @@ namespace CryptoExchange.Net
|
||||
log.Level = exchangeOptions.LogVerbosity;
|
||||
|
||||
apiProxy = exchangeOptions.Proxy;
|
||||
if(apiProxy != null)
|
||||
if (apiProxy != null)
|
||||
log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}");
|
||||
|
||||
rateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
|
||||
@ -52,7 +58,7 @@ namespace CryptoExchange.Net
|
||||
foreach (var rateLimiter in exchangeOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.
|
||||
/// </summary>
|
||||
@ -69,7 +75,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
rateLimiters.Clear();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Set the authentication provider
|
||||
/// </summary>
|
||||
@ -82,8 +88,9 @@ 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
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, $"Creating request for " + uri);
|
||||
if (signed && authProvider == null)
|
||||
{
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, $"Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
return new CallResult<T>(null, new NoApiCredentialsError());
|
||||
}
|
||||
@ -91,7 +98,10 @@ namespace CryptoExchange.Net
|
||||
var request = ConstructRequest(uri, method, parameters, signed);
|
||||
|
||||
if (apiProxy != null)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Setting proxy");
|
||||
request.SetProxy(apiProxy.Host, apiProxy.Port);
|
||||
}
|
||||
|
||||
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, $"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);
|
||||
return result.Error != null ? new CallResult<T>(null, result.Error) : Deserialize<T>(result.Data);
|
||||
}
|
||||
@ -119,7 +140,14 @@ namespace CryptoExchange.Net
|
||||
if (!uriString.EndsWith("?"))
|
||||
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)
|
||||
@ -185,22 +213,27 @@ namespace CryptoExchange.Net
|
||||
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
|
||||
{
|
||||
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;
|
||||
var ary = (JArray)obj;
|
||||
if (ary.HasValues && ary[0] is JObject jObject)
|
||||
CheckObject(typeof(T).GetElementType(), jObject);
|
||||
CheckObject(typeof(T).GetElementType(), jObject);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -208,18 +241,24 @@ namespace CryptoExchange.Net
|
||||
log.Write(LogVerbosity.Debug, "Failed to check response data: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(), null);
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer), null);
|
||||
}
|
||||
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);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
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);
|
||||
return new CallResult<T>(null, new DeserializeError(info));
|
||||
}
|
||||
@ -227,6 +266,19 @@ namespace CryptoExchange.Net
|
||||
|
||||
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();
|
||||
@ -237,7 +289,7 @@ namespace CryptoExchange.Net
|
||||
if (ignore != null)
|
||||
continue;
|
||||
|
||||
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute) attr).PropertyName);
|
||||
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName);
|
||||
}
|
||||
foreach (var token in obj)
|
||||
{
|
||||
@ -246,8 +298,8 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower());
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
@ -259,20 +311,26 @@ namespace CryptoExchange.Net
|
||||
continue;
|
||||
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]);
|
||||
else if(token.Value is JObject)
|
||||
else if (token.Value is JObject)
|
||||
CheckObject(propType, (JObject)token.Value);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -288,7 +346,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
else
|
||||
{
|
||||
if (((JsonPropertyAttribute) attr).PropertyName == name)
|
||||
if (((JsonPropertyAttribute)attr).PropertyName == name)
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
@ -310,6 +368,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
authProvider?.Credentials?.Dispose();
|
||||
log.Write(LogVerbosity.Debug, "Disposing exchange client");
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@ -25,5 +28,30 @@ namespace CryptoExchange.Net
|
||||
if (value != null)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using SuperSocket.ClientEngine;
|
||||
using SuperSocket.ClientEngine.Proxy;
|
||||
using WebSocket4Net;
|
||||
|
||||
@ -14,6 +17,8 @@ namespace CryptoExchange.Net.Implementation
|
||||
public class BaseSocket: IWebsocket
|
||||
{
|
||||
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> openhandlers = new List<Action>();
|
||||
@ -35,12 +40,13 @@ namespace CryptoExchange.Net.Implementation
|
||||
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.EnableAutoSendPing = true;
|
||||
socket.AutoSendPingInterval = 10;
|
||||
@ -70,7 +76,7 @@ namespace CryptoExchange.Net.Implementation
|
||||
public event Action OnOpen
|
||||
{
|
||||
add => openhandlers.Add(value);
|
||||
remove => closehandlers.Remove(value);
|
||||
remove => openhandlers.Remove(value);
|
||||
}
|
||||
|
||||
protected static void Handle(List<Action> handlers)
|
||||
@ -89,12 +95,23 @@ namespace CryptoExchange.Net.Implementation
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
ManualResetEvent evnt = new ManualResetEvent(false);
|
||||
var handler = new EventHandler((o, a) => evnt.Set());
|
||||
socket.Closed += handler;
|
||||
socket.Close();
|
||||
evnt.WaitOne();
|
||||
socket.Closed -= handler;
|
||||
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);
|
||||
var handler = new EventHandler((o, a) => evnt.Set());
|
||||
socket.Closed += handler;
|
||||
socket.Close();
|
||||
bool triggered = evnt.WaitOne(3000);
|
||||
socket.Closed -= handler;
|
||||
log.Write(LogVerbosity.Debug, "Websocket closed");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -107,15 +124,34 @@ namespace CryptoExchange.Net.Implementation
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
ManualResetEvent evnt = new ManualResetEvent(false);
|
||||
var handler = new EventHandler((o, a) => evnt.Set());
|
||||
socket.Opened += handler;
|
||||
socket.Closed += handler;
|
||||
socket.Open();
|
||||
evnt.WaitOne();
|
||||
socket.Opened -= handler;
|
||||
socket.Closed -= handler;
|
||||
return socket.State == WebSocketState.Open;
|
||||
bool connected;
|
||||
lock (socketLock)
|
||||
{
|
||||
log.Write(LogVerbosity.Debug, "Connecting websocket");
|
||||
ManualResetEvent evnt = new ManualResetEvent(false);
|
||||
var handler = new EventHandler((o, a) => evnt?.Set());
|
||||
var errorHandler = new EventHandler<ErrorEventArgs>((o, a) => evnt?.Set());
|
||||
socket.Opened += handler;
|
||||
socket.Closed += handler;
|
||||
socket.Error += errorHandler;
|
||||
socket.Open();
|
||||
evnt.WaitOne(TimeSpan.FromSeconds(15));
|
||||
socket.Opened -= handler;
|
||||
socket.Closed -= handler;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -134,12 +170,19 @@ namespace CryptoExchange.Net.Implementation
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
socket?.Dispose();
|
||||
lock (socketLock)
|
||||
{
|
||||
if (socket != null)
|
||||
log.Write(LogVerbosity.Debug, "Disposing websocket");
|
||||
|
||||
errorhandlers.Clear();
|
||||
openhandlers.Clear();
|
||||
closehandlers.Clear();
|
||||
messagehandlers.Clear();
|
||||
socket?.Dispose();
|
||||
socket = null;
|
||||
|
||||
errorhandlers.Clear();
|
||||
openhandlers.Clear();
|
||||
closehandlers.Clear();
|
||||
messagehandlers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
CryptoExchange.Net/Implementation/TestWebsocket.cs
Normal file
87
CryptoExchange.Net/Implementation/TestWebsocket.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
13
CryptoExchange.Net/Implementation/TestWebsocketFactory.cs
Normal file
13
CryptoExchange.Net/Implementation/TestWebsocketFactory.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Implementation
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
using CryptoExchange.Net.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
public interface IWebsocketFactory
|
||||
{
|
||||
IWebsocket CreateWebsocket(string url);
|
||||
IWebsocket CreateWebsocket(Log log, string url);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ namespace CryptoExchange.Net.Logging
|
||||
private List<TextWriter> writers;
|
||||
private LogVerbosity level = LogVerbosity.Info;
|
||||
|
||||
|
||||
public LogVerbosity Level
|
||||
{
|
||||
get => level;
|
||||
@ -36,16 +37,19 @@ namespace CryptoExchange.Net.Logging
|
||||
|
||||
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
|
||||
{
|
||||
if ((int) logType >= (int) Level)
|
||||
writer.WriteLine($"{DateTime.Now:yyyy/MM/dd hh:mm:ss:fff} | {logType} | {message}");
|
||||
writer.WriteLine(logMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine("Failed to write log: " + e.Message);
|
||||
Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
44
CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs
Normal file
44
CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Requests
|
||||
|
||||
public async Task<Stream> GetRequestStream()
|
||||
{
|
||||
return await request.GetRequestStreamAsync();
|
||||
return await request.GetRequestStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IResponse> GetResponse()
|
||||
|
Loading…
x
Reference in New Issue
Block a user