mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d91755dff5 | |||
| 27d49a6093 | |||
| ad1bdd9a3f | |||
| 4f2d7abc7e | |||
| ef9de5e338 | |||
| 8b513e51b9 | |||
| d43b38a23a | |||
| 0987c0f9d1 | |||
| e2dde77023 | |||
| 104ac7caad | |||
| 8788dd3deb | |||
| f64cc5e9cf | |||
| 75d1bbc6e8 | |||
| b621aa7e65 | |||
| 9783108695 | |||
| 6ba32fe280 | |||
| f75cc75bbc | |||
| 2109b65a8e | |||
| a472751638 | |||
| f08ed16f2a | |||
| 212d457a6a | |||
| ac5f333766 | |||
| 640e4387c1 | |||
| a16b19019f | |||
| 2443f576ac | |||
| 4fd7e44015 | |||
| a0a3bda1c5 | |||
| 6bda7a3c73 | |||
| 69a7a714cd | |||
| 48e2e6468e | |||
| 4017ac780f | |||
| a55cd1bb13 |
@@ -115,6 +115,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
@@ -143,6 +144,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
@@ -162,6 +164,44 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", null)]
|
||||
public void TestBoolConverter(string value, bool? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", false)]
|
||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
}
|
||||
|
||||
public class TimeObject
|
||||
@@ -180,6 +220,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
public class BoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableBoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public enum TestEnum
|
||||
{
|
||||
|
||||
@@ -60,11 +60,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public override string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
}
|
||||
|
||||
@@ -74,6 +74,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA256Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -87,6 +98,19 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -100,6 +124,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -113,6 +172,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -127,28 +221,70 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignMD5Bytes(string data)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
@@ -182,7 +318,46 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
using var rsa = CreateRSA();
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha384 = SHA384.Create();
|
||||
var hash = sha384.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha512 = SHA512.Create();
|
||||
var hash = sha512.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
@@ -209,30 +384,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a byte array
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <returns></returns>
|
||||
public virtual byte[] Sign(byte[] toSign)
|
||||
{
|
||||
return toSign;
|
||||
return rsa;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
@@ -78,13 +79,9 @@ namespace CryptoExchange.Net
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// The default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
@@ -204,7 +201,7 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
serializer ??= DefaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -242,7 +239,7 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
serializer ??= DefaultSerializer;
|
||||
string? data = null;
|
||||
|
||||
try
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public virtual void Initialize(ExchangeOptions options)
|
||||
protected virtual void Initialize(ExchangeOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
@@ -15,7 +15,6 @@ using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static CryptoExchange.Net.Objects.RateLimiter;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -84,6 +83,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
@@ -98,6 +98,7 @@ namespace CryptoExchange.Net
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
@@ -109,11 +110,16 @@ namespace CryptoExchange.Net
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||
if (!result)
|
||||
_logger.Log(LogLevel.Warning, $"[{result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
|
||||
else
|
||||
_logger.Log(LogLevel.Debug, $"[{result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
|
||||
|
||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
@@ -130,6 +136,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
@@ -144,6 +151,7 @@ namespace CryptoExchange.Net
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
@@ -156,11 +164,16 @@ namespace CryptoExchange.Net
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult<T>(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||
if (!result)
|
||||
_logger.Log(LogLevel.Warning, $"[{result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
|
||||
else
|
||||
_logger.Log(LogLevel.Debug, $"[{result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
|
||||
|
||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
@@ -176,6 +189,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
@@ -189,6 +203,7 @@ namespace CryptoExchange.Net
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
@@ -233,7 +248,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
_logger.Log(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders);
|
||||
|
||||
string? paramString = "";
|
||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||
@@ -262,9 +277,9 @@ namespace CryptoExchange.Net
|
||||
CancellationToken cancellationToken,
|
||||
bool expectedEmptyResponse)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
var statusCode = response.StatusCode;
|
||||
@@ -274,7 +289,7 @@ namespace CryptoExchange.Net
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
||||
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
||||
// response before being able to deserialize it into the resulting type since we don't know if its an error response or data
|
||||
if (manualParseError)
|
||||
{
|
||||
using var reader = new StreamReader(responseStream);
|
||||
@@ -282,23 +297,22 @@ namespace CryptoExchange.Net
|
||||
responseLength ??= data.Length;
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
_logger.Log(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(OutputOriginalData ? (": " + data) : "")}");
|
||||
|
||||
if (!expectedEmptyResponse)
|
||||
{
|
||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
// Let the library implementation see if it is an error response, and if so parse the error
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
|
||||
// Not an error, so continue deserializing
|
||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -307,16 +321,16 @@ namespace CryptoExchange.Net
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
// Not empty, and not json
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
// Error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
}
|
||||
|
||||
// Empty success response; okay
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -327,7 +341,7 @@ namespace CryptoExchange.Net
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
}
|
||||
|
||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||
@@ -335,7 +349,7 @@ namespace CryptoExchange.Net
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, responseLength, OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, responseLength, OutputOriginalData ? desResult.OriginalData : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -343,7 +357,6 @@ namespace CryptoExchange.Net
|
||||
// Http status code indicates error
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
@@ -355,29 +368,26 @@ namespace CryptoExchange.Net
|
||||
|
||||
if (error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +423,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
||||
/// <param name="bodyFormat">Format of the body content</param>
|
||||
/// <param name="requestId">Unique id of a request</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <returns></returns>
|
||||
@@ -423,6 +434,7 @@ namespace CryptoExchange.Net
|
||||
bool signed,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
RequestBodyFormat bodyFormat,
|
||||
int requestId,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
@@ -503,7 +515,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
WriteParamBody(request, bodyParameters, contentType);
|
||||
else
|
||||
@@ -521,13 +533,13 @@ namespace CryptoExchange.Net
|
||||
/// <param name="contentType">The content type of the data</param>
|
||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||
{
|
||||
if (requestBodyFormat == RequestBodyFormat.Json)
|
||||
if (contentType == Constants.JsonContentHeader)
|
||||
{
|
||||
// Write the parameters as json in the body
|
||||
var stringData = JsonConvert.SerializeObject(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (requestBodyFormat == RequestBodyFormat.FormData)
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
@@ -558,7 +570,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
// Handle retry after header
|
||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (!retryAfterHeader.Value.Any())
|
||||
if (retryAfterHeader.Value?.Any() != true)
|
||||
return new ServerRateLimitError(data);
|
||||
|
||||
var value = retryAfterHeader.Value.First();
|
||||
@@ -581,14 +593,14 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
@@ -617,7 +629,7 @@ namespace CryptoExchange.Net
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ namespace CryptoExchange.Net
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
_logger.Log(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} Attempting to authenticate");
|
||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
@@ -414,6 +414,7 @@ namespace CryptoExchange.Net
|
||||
return new CallResult<bool>(result.Error);
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} authenticated");
|
||||
socket.Authenticated = true;
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
@@ -511,7 +512,7 @@ namespace CryptoExchange.Net
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
||||
dataHandler(new DataEvent<T>(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
dataHandler(new DataEvent<T>(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp, null));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -522,7 +523,7 @@ namespace CryptoExchange.Net
|
||||
return;
|
||||
}
|
||||
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp, null));
|
||||
}
|
||||
|
||||
var subscription = request == null
|
||||
@@ -562,7 +563,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||
{
|
||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||
}
|
||||
@@ -572,7 +573,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="request">The original request</param>
|
||||
/// <returns></returns>
|
||||
public virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
|
||||
protected internal virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
|
||||
{
|
||||
return Task.FromResult(new CallResult<object>(request));
|
||||
}
|
||||
@@ -682,7 +683,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="identifier">Identifier for the periodic send</param>
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
protected virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
{
|
||||
if (objGetter == null)
|
||||
throw new ArgumentNullException(nameof(objGetter));
|
||||
|
||||
@@ -109,13 +109,21 @@ namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
if (token.Type == JTokenType.Null)
|
||||
value = null;
|
||||
|
||||
if (token.Type == JTokenType.Float)
|
||||
value = token.Value<decimal>();
|
||||
}
|
||||
|
||||
if ((property.PropertyType == typeof(decimal)
|
||||
if (value is decimal)
|
||||
{
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
var v = value.ToString();
|
||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
property.SetValue(result, dec);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Boolean converter with support for "0"/"1" (strings)
|
||||
/// </summary>
|
||||
public class BoolConverter : JsonConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this instance can convert the specified object type.
|
||||
/// </summary>
|
||||
/// <param name="objectType">Type of the object.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
|
||||
return objectType == typeof(bool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the JSON representation of the object.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
|
||||
/// <param name="objectType">Type of the object.</param>
|
||||
/// <param name="existingValue">The existing value of object being read.</param>
|
||||
/// <param name="serializer">The calling serializer.</param>
|
||||
/// <returns>
|
||||
/// The object value.
|
||||
/// </returns>
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var value = reader.Value?.ToString().ToLower().Trim();
|
||||
if (value == null || value == "")
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
case "0":
|
||||
case "off":
|
||||
case "-1":
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
|
||||
return new JsonSerializer().Deserialize(reader, objectType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that this converter will not participate in writing results.
|
||||
/// </summary>
|
||||
public override bool CanWrite { get { return false; } }
|
||||
|
||||
/// <summary>
|
||||
/// Writes the JSON representation of the object.
|
||||
/// </summary>
|
||||
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,12 @@ namespace CryptoExchange.Net.Converters
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue == "-1"
|
||||
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
||||
{
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
}
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Kraken.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for serializing decimal values as string
|
||||
/// </summary>
|
||||
public class DecimalStringWriterConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,29 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
public class EnumConverter : JsonConverter
|
||||
{
|
||||
private bool _warnOnMissingEntry = true;
|
||||
private bool _writeAsInt;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public EnumConverter() { }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="writeAsInt"></param>
|
||||
/// <param name="warnOnMissingEntry"></param>
|
||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
||||
{
|
||||
_writeAsInt = writeAsInt;
|
||||
_warnOnMissingEntry = warnOnMissingEntry;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsEnum;
|
||||
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -51,8 +68,12 @@ namespace CryptoExchange.Net.Converters
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
{
|
||||
// We received an enum value but weren't able to parse it.
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
||||
if (_warnOnMissingEntry)
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -117,22 +138,39 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString<T>(T enumValue)
|
||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
private static string? GetString(Type objectType, object? enumValue)
|
||||
{
|
||||
var objectType = typeof(T);
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = GetString(value);
|
||||
writer.WriteValue(stringValue);
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_writeAsInt)
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteValue(stringValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteValue((int)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer options
|
||||
/// </summary>
|
||||
public static class SerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
Converters =
|
||||
{
|
||||
new EnumConverter(),
|
||||
new DateTimeConverter(),
|
||||
new BoolConverter()
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Default json serializer settings
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings Default => new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,16 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>6.1.0</PackageVersion>
|
||||
<AssemblyVersion>6.1.0</AssemblyVersion>
|
||||
<FileVersion>6.1.0</FileVersion>
|
||||
<PackageVersion>6.2.5</PackageVersion>
|
||||
<AssemblyVersion>6.2.5</AssemblyVersion>
|
||||
<FileVersion>6.2.5</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>6.1.0 - Added support for ratelimiting on socket connections, Added rest ratelimit handling and parsing, Added ServerRatelimitError error</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>6.2.5 - Added support for deserializing null and empty string values to BoolConverter</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -8,6 +9,8 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class ExchangeHelpers
|
||||
{
|
||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
@@ -128,7 +131,6 @@ namespace CryptoExchange.Net
|
||||
return value / 1.000000000000000000000000000000000m;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||
/// </summary>
|
||||
@@ -141,5 +143,43 @@ namespace CryptoExchange.Net
|
||||
return _lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="length">Length of the random string</param>
|
||||
/// <returns></returns>
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
var randomChars = new char[length];
|
||||
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||
#else
|
||||
var random = new Random();
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
||||
#endif
|
||||
|
||||
return new string(randomChars);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="source">The initial string</param>
|
||||
/// <param name="totalLength">Total length of the resulting string</param>
|
||||
/// <returns></returns>
|
||||
public static string AppendRandomString(string source, int totalLength)
|
||||
{
|
||||
if (totalLength < source.Length)
|
||||
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
||||
|
||||
if (totalLength == source.Length)
|
||||
return source;
|
||||
|
||||
return source + RandomString(totalLength - source.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,5 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <summary>
|
||||
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
TimeSyncInfo? GetTimeSyncInfo();
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to reconnect to after losing a connection
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
@@ -45,19 +37,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <returns></returns>
|
||||
Task ReconnectAsync();
|
||||
/// <summary>
|
||||
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||
/// </summary>
|
||||
/// <param name="request">The original request</param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<object>> RevitalizeRequestAsync(object request);
|
||||
/// <summary>
|
||||
/// Periodically sends data over a socket connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">Identifier for the periodic send</param>
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -186,6 +186,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
@@ -217,6 +222,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
@@ -226,6 +232,7 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
@@ -235,6 +242,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
RequestId = requestId;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
@@ -255,7 +263,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -281,6 +289,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
@@ -319,6 +332,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="responseLength"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
@@ -331,6 +345,7 @@ namespace CryptoExchange.Net.Objects
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
@@ -343,6 +358,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ResponseTime = responseTime;
|
||||
ResponseLength = responseLength;
|
||||
|
||||
RequestId = requestId;
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
@@ -355,7 +371,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
@@ -363,14 +379,14 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, default, error) { }
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
@@ -380,7 +396,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -391,7 +407,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -124,4 +124,19 @@
|
||||
/// </summary>
|
||||
Closest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of the update
|
||||
/// </summary>
|
||||
public enum SocketUpdateType
|
||||
{
|
||||
/// <summary>
|
||||
/// A update
|
||||
/// </summary>
|
||||
Update,
|
||||
/// <summary>
|
||||
/// A snapshot, generally send at the start of the connection
|
||||
/// </summary>
|
||||
Snapshot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Code != null ? $"{Code}: {Message} {Data}" : $"{Message} {Data}";
|
||||
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters collection
|
||||
/// </summary>
|
||||
public class ParameterCollection : Dictionary<string, object>
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptional(string key, object? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, decimal value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, decimal? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, int value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, int? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, long value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, long? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddMilliseconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalMilliseconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddMillisecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalMillisecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as seconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddSeconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as seconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalSeconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddEnum<T>(string key, T value)
|
||||
{
|
||||
Add(key, EnumConverter.GetString(value)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalEnum<T>(string key, T? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, EnumConverter.GetString(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,10 +117,10 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
int totalWaitTime = 0;
|
||||
|
||||
EndpointRateLimiter? endpointLimit;
|
||||
List<EndpointRateLimiter> endpointLimits;
|
||||
lock (_limiterLock)
|
||||
endpointLimit = _limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||
if(endpointLimit != null)
|
||||
endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
|
||||
foreach (var endpointLimit in endpointLimits)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
@@ -129,7 +129,7 @@ namespace CryptoExchange.Net.Objects
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
if (endpointLimit?.IgnoreOtherRateLimits == true)
|
||||
if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||
@@ -169,10 +169,10 @@ namespace CryptoExchange.Net.Objects
|
||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
ApiKeyRateLimiter? apiLimit;
|
||||
List<ApiKeyRateLimiter> apiLimits;
|
||||
lock (_limiterLock)
|
||||
apiLimit = _limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
||||
if (apiLimit != null)
|
||||
apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
|
||||
foreach (var apiLimit in apiLimits)
|
||||
{
|
||||
if(apiKey == null)
|
||||
{
|
||||
@@ -206,13 +206,13 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
|
||||
if ((signed || apiLimits.All(l => !l.OnlyForSignedRequests)) && apiLimits.Any(l => l.IgnoreTotalRateLimit))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
TotalRateLimiter? totalLimit;
|
||||
List<TotalRateLimiter> totalLimits;
|
||||
lock (_limiterLock)
|
||||
totalLimit = _limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||
if (totalLimit != null)
|
||||
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
|
||||
foreach(var totalLimit in totalLimits)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
|
||||
@@ -464,7 +464,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
var pbList = _processBuffer.ToList();
|
||||
if (pbList.Count > 0)
|
||||
_logger.Log(LogLevel.Debug, $"Processing {pbList.Count} buffered updates");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} Processing {pbList.Count} buffered updates");
|
||||
|
||||
foreach (var bufferEntry in pbList)
|
||||
{
|
||||
@@ -661,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (_stopProcessing)
|
||||
{
|
||||
_logger.Log(LogLevel.Trace, "Skipping message because of resubscribing");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
return;
|
||||
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} to send buffer");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} bytes to send buffer");
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||
_sendEvent.Set();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
@@ -23,35 +24,23 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of update
|
||||
/// </summary>
|
||||
public SocketUpdateType? UpdateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The received data deserialized into an object
|
||||
/// </summary>
|
||||
public T Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ctor
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="timestamp"></param>
|
||||
public DataEvent(T data, DateTime timestamp)
|
||||
{
|
||||
Data = data;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
internal DataEvent(T data, string? topic, DateTime timestamp)
|
||||
{
|
||||
Data = data;
|
||||
Topic = topic;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp)
|
||||
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
||||
{
|
||||
Data = data;
|
||||
Topic = topic;
|
||||
OriginalData = originalData;
|
||||
Timestamp = timestamp;
|
||||
UpdateType = updateType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,7 +51,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data)
|
||||
{
|
||||
return new DataEvent<K>(data, Topic, OriginalData, Timestamp);
|
||||
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +63,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string? topic)
|
||||
{
|
||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp);
|
||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <param name="topic">The new topic</param>
|
||||
/// <param name="updateType">The type of update</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType)
|
||||
{
|
||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||
if (!reconnectSuccessful)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"Failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
|
||||
await _socket.ReconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -291,10 +291,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="requestId">Id of the request sent</param>
|
||||
protected virtual void HandleRequestSent(int requestId)
|
||||
{
|
||||
var pendingRequest = _pendingRequests.SingleOrDefault(p => p.Id == requestId);
|
||||
PendingRequest pendingRequest;
|
||||
lock (_pendingRequests)
|
||||
pendingRequest = _pendingRequests.SingleOrDefault(p => p.Id == requestId);
|
||||
|
||||
if (pendingRequest == null)
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} - msg {requestId} - message sent, but not pending");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} - msg {requestId} - message sent, but not pending");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,7 +348,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Answer to a timed out request, unsub if it is a subscription request
|
||||
if (pendingRequest.Subscription != null)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the RequestTimeout");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Received subscription info after request timed out; unsubscribing. Consider increasing the RequestTimeout");
|
||||
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +383,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -711,7 +714,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription.Request!).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, "Failed request revitalization: " + result.Error);
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Failed request revitalization: " + result.Error);
|
||||
return result.As<bool>(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.0.0" />
|
||||
<PackageReference Include="Binance.Net" Version="9.1.5" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.2.1" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.0.3" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="1.4.2" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
@page "/"
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBitgetRestClient bitgetClient
|
||||
@inject IBittrexRestClient bittrexClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject IHuobiRestClient huobiClient
|
||||
@inject IKrakenRestClient krakenClient
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
@inject IOKXRestClient okxClient
|
||||
|
||||
<h3>BTC-USD prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -21,12 +23,14 @@
|
||||
{
|
||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
|
||||
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
|
||||
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
|
||||
|
||||
@@ -36,6 +40,9 @@
|
||||
if (bitfinexTask.Result.Success)
|
||||
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
|
||||
|
||||
if (bitgetTask.Result.Success)
|
||||
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
|
||||
|
||||
if (bittrexTask.Result.Success)
|
||||
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
||||
|
||||
@@ -53,6 +60,9 @@
|
||||
|
||||
if (kucoinTask.Result.Success)
|
||||
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (okxTask.Result.Success)
|
||||
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
@page "/LiveData"
|
||||
@inject IBinanceSocketClient binanceSocketClient
|
||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||
@inject IBitgetSocketClient bitgetSocketClient
|
||||
@inject IBittrexSocketClient bittrexSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@inject IHuobiSocketClient huobiSocketClient
|
||||
@inject IKrakenSocketClient krakenSocketClient
|
||||
@inject IKucoinSocketClient kucoinSocketClient
|
||||
@inject IOKXSocketClient okxSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
@using CryptoExchange.Net.Objects
|
||||
@using CryptoExchange.Net.Sockets
|
||||
@@ -28,12 +30,14 @@
|
||||
{
|
||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
||||
bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.LastPrice ?? 0)),
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using System.Timers
|
||||
@using Binance.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using Bittrex.Net.Interfaces
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@@ -11,14 +12,17 @@
|
||||
@using Kraken.Net.Interfaces
|
||||
@using Kucoin.Net.Clients
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@inject IBinanceOrderBookFactory binanceFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@inject IBitgetOrderBookFactory bitgetFactory
|
||||
@inject IBittrexOrderBookFactory bittrexFactory
|
||||
@inject IBybitOrderBookFactory bybitFactory
|
||||
@inject ICoinExOrderBookFactory coinExFactory
|
||||
@inject IHuobiOrderBookFactory huobiFactory
|
||||
@inject IKrakenOrderBookFactory krakenFactory
|
||||
@inject IKucoinOrderBookFactory kucoinFactory
|
||||
@inject IOKXOrderBookFactory okxFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC books, live updates:</h3>
|
||||
@@ -54,12 +58,14 @@
|
||||
{
|
||||
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
@page "/SpotClient"
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBitgetRestClient bitgetClient
|
||||
@inject IBittrexRestClient bittrexClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject IHuobiRestClient huobiClient
|
||||
@inject IKrakenRestClient krakenClient
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
@inject IOKXRestClient okxClient
|
||||
@using Binance.Net.Clients.SpotApi
|
||||
@using Bitfinex.Net.Clients.SpotApi
|
||||
@using Bittrex.Net.Clients.SpotApi
|
||||
@using Bitget.Net.Clients.SpotApi
|
||||
@using Bybit.Net.Clients.SpotApi
|
||||
@using CoinEx.Net.Clients.SpotApi
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@@ -17,6 +20,7 @@
|
||||
@using Huobi.Net.Clients.SpotApi
|
||||
@using Kraken.Net.Clients.SpotApi
|
||||
@using Kucoin.Net.Clients.SpotApi
|
||||
@using OKX.Net.Clients.UnifiedApi
|
||||
|
||||
<h3>ETH-BTC prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -34,12 +38,14 @@
|
||||
|
||||
binanceClient.SpotApi.CommonSpotClient,
|
||||
bitfinexClient.SpotApi.CommonSpotClient,
|
||||
bittrexClient.SpotApi.CommonSpotClient,
|
||||
bitgetClient.SpotApi.CommonSpotClient,
|
||||
bittrexClient.SpotApi.CommonSpotClient,
|
||||
bybitClient.SpotApiV1.CommonSpotClient,
|
||||
coinexClient.SpotApi.CommonSpotClient,
|
||||
huobiClient.SpotApi.CommonSpotClient,
|
||||
krakenClient.SpotApi.CommonSpotClient,
|
||||
kucoinClient.SpotApi.CommonSpotClient
|
||||
kucoinClient.SpotApi.CommonSpotClient,
|
||||
okxClient.UnifiedApi.CommonSpotClient
|
||||
};
|
||||
|
||||
var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC"))));
|
||||
|
||||
@@ -3,6 +3,7 @@ using Binance.Net;
|
||||
using Binance.Net.Clients;
|
||||
using Binance.Net.Interfaces.Clients;
|
||||
using Bitfinex.Net;
|
||||
using Bitget.Net;
|
||||
using Bittrex.Net;
|
||||
using Bybit.Net;
|
||||
using CoinEx.Net;
|
||||
@@ -16,6 +17,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OKX.Net;
|
||||
|
||||
namespace BlazorClient
|
||||
{
|
||||
@@ -48,12 +50,14 @@ namespace BlazorClient
|
||||
});
|
||||
|
||||
services.AddBitfinex();
|
||||
services.AddBitget();
|
||||
services.AddBittrex();
|
||||
services.AddBybit();
|
||||
services.AddCoinEx();
|
||||
services.AddHuobi();
|
||||
services.AddKraken();
|
||||
services.AddKucoin();
|
||||
services.AddOKX();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
@using BlazorClient.Shared
|
||||
@using Binance.Net.Interfaces.Clients;
|
||||
@using Bitfinex.Net.Interfaces.Clients;
|
||||
@using Bitget.Net.Interfaces.Clients;
|
||||
@using Bittrex.Net.Interfaces.Clients;
|
||||
@using Bybit.Net.Interfaces.Clients;
|
||||
@using CoinEx.Net.Interfaces.Clients;
|
||||
@using Huobi.Net.Interfaces.Clients;
|
||||
@using Kraken.Net.Interfaces.Clients;
|
||||
@using Kucoin.Net.Interfaces.Clients;
|
||||
@using Kucoin.Net.Interfaces.Clients;
|
||||
@using OKX.Net.Interfaces.Clients;
|
||||
@@ -6,14 +6,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.0.0" />
|
||||
<PackageReference Include="Binance.Net" Version="9.1.5" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.2.1" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.0.3" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="1.4.2" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# CryptoExchange.Net
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml)  
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) [](https://www.nuget.org/packages/CryptoExchange.Net)
|
||||
|
||||
CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections.
|
||||
|
||||
@@ -18,7 +18,6 @@ Use one of the following following referral links to signup to a new exchange to
|
||||
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||
[FTX](https://ftx.com/referrals#a=31620192)
|
||||
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||
|
||||
@@ -32,6 +31,50 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 6.2.5 - 09 Jan 2024
|
||||
* Added support for deserializing null and empty string values to BoolConverter
|
||||
|
||||
* Version 6.2.4 - 04 Jan 2024
|
||||
* Fixed parsing of DateTime value of zero with additional zero decimal places
|
||||
|
||||
* Version 6.2.3 - 02 Dec 2023
|
||||
* Fixed requestBodyFormat parameter handling
|
||||
|
||||
* Version 6.2.2 - 02 Dec 2023
|
||||
* Added support for specifying the request body content type on a per request basis
|
||||
* Added DecimalStringWriterConverter
|
||||
* Added RequestId to WebCallResult model
|
||||
* Updated response logging
|
||||
|
||||
* Version 6.2.1 - 28 Oct 2023
|
||||
* Utility methods
|
||||
|
||||
* Version 6.2.0 - 24 Oct 2023
|
||||
* Added SerializerOptions helper class for setting a default serializer
|
||||
* Added ParameterCollection helper class for easier parameter definition
|
||||
* Added extra helper methods AuthenticationProvider
|
||||
* Remove interface entries meant for internal use
|
||||
* Added support for writing int values to the EnumConverter
|
||||
|
||||
* Version 6.1.5 - 08 Oct 2023
|
||||
* Added UpdateType to socket DataEvent
|
||||
* Added additional scenarios for BoolConverter
|
||||
* Updated some logging
|
||||
|
||||
* Version 6.1.4 - 23 Sep 2023
|
||||
* Added BoolConverter
|
||||
* Added parameter for logging warning message on missing enum entry to EnumConverter
|
||||
|
||||
* Version 6.1.3 - 18 Sep 2023
|
||||
* Fix for concurrency exception in socket subscription
|
||||
|
||||
* Version 6.1.2 - 11 Sep 2023
|
||||
* Added support for multiple of the same ratelimiting type in the same rate limiter
|
||||
* Fixed nullreference on rate limit error if no Retry-After header is returned
|
||||
|
||||
* Version 6.1.1 - 04 Sep 2023
|
||||
* Fixes for json converters
|
||||
|
||||
* Version 6.1.0 - 24 Aug 2023
|
||||
* Added support for ratelimiting on socket connections
|
||||
* Added rest ratelimit handling and parsing
|
||||
|
||||
+4
-4
@@ -82,7 +82,7 @@ All clients have access to the following options, specific implementations might
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time sure to be in sync.|`true`|
|
||||
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time sure is to be in sync.|`true`|
|
||||
|`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed| `TimeSpan.FromHours(1)`
|
||||
|`Environment`|The environment the library should talk to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments|`Live environment`
|
||||
|
||||
@@ -104,8 +104,8 @@ All clients have access to the following options, specific implementations might
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`ApiCredentials`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update. Overrides the Base client options `OutputOriginalData` option if set| `false`
|
||||
|`OutputOriginalData`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example. Available base addresses are defined in the [Library]ApiAddresses helper class, for example `KucoinApiAddresses`|Depends on implementation
|
||||
|`ApiCredentials`|The API credentials to use for accessing protected endpoints. Can either be an API key/secret using Hmac encryption or an API key/private key using RSA encryption for exchanges that support that. See [Credentials](#credentials). Setting ApiCredentials on the Api Options will override any default ApiCredentials on the `Base client options`| `null`
|
||||
|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update.|False
|
||||
|
||||
**Options for Rest Api Client (extension of base api client options)**
|
||||
|
||||
@@ -113,7 +113,7 @@ All clients have access to the following options, specific implementations might
|
||||
|------|-----------|-------|
|
||||
|`RateLimiters`|A list of `IRateLimiter`s to use.|`new List<IRateLimiter>()`|
|
||||
|`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`|
|
||||
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time sure to be in sync. Overrides the Rest client options `AutoTimestamp` option if set|`null`|
|
||||
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time is sure to be in sync. Overrides the Rest client options `AutoTimestamp` option if set|`null`|
|
||||
|`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed. Overrides the Rest client options `TimestampRecalculationInterval` option if set| `TimeSpan.FromHours(1)`
|
||||
|
||||
**Options for Socket Api Client (extension of base api client options)**
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@ title: Home
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml)  
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) [](https://www.nuget.org/packages/CryptoExchange.Net)
|
||||
|
||||
The CryptoExchange.Net library is a base package for exchange API implementations. It offers base classes for creating clients for exchange API's. Basing exchange implementation on the common CryptoExchange.Net library allows for ease of implementation for new exchanges, as only the endpoints and models have to implemented, but not all systems around requests and connections, and it makes it easier for users to implement a new library in their code base as all base principles and configuration are the same for different exchanges.
|
||||
|
||||
@@ -14,6 +14,7 @@ These will always be on the latest CryptoExchange.Net version and the latest ver
|
||||
|-|-|-|
|
||||
|<a href="https://github.com/JKorf/Binance.Net"><img src="https://github.com/JKorf/Binance.Net/blob/master/Binance.Net/Icon/icon.png?raw=true"></a>|Binance|https://jkorf.github.io/Binance.Net/|
|
||||
|<a href="https://github.com/JKorf/Bitfinex.Net"><img src="https://github.com/JKorf/Bitfinex.Net/blob/master/Bitfinex.Net/Icon/icon.png?raw=true"></a>|Bitfinex|https://jkorf.github.io/Bitfinex.Net/|
|
||||
|<a href="https://github.com/JKorf/Bitget.Net"><img src="https://github.com/JKorf/Bitget.Net/blob/master/Bitget.Net/Icon/icon.png?raw=true"></a>|Bitget|https://jkorf.github.io/Bitget.Net/|
|
||||
|<a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Bittrex.Net/Icon/icon.png?raw=true"></a>|Bittrex|https://jkorf.github.io/Bittrex.Net/|
|
||||
|<a href="https://github.com/JKorf/Bybit.Net"><img src="https://github.com/JKorf/Bybit.Net/blob/main/ByBit.Net/Icon/icon.png?raw=true"></a>|Bybit|https://jkorf.github.io/Bybit.Net/|
|
||||
|<a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/CoinEx.Net/Icon/icon.png?raw=true"></a>|CoinEx|https://jkorf.github.io/CoinEx.Net/|
|
||||
|
||||
Reference in New Issue
Block a user