mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| b34129e148 | |||
| be25a68c9c | |||
| 468cd5e48e | |||
| 262c4e4aa5 | |||
| 4ccff6461f | |||
| 3bfa3ef389 | |||
| 5238971bcc | |||
| 2f5c904faf | |||
| 8d35339ab2 |
@@ -115,6 +115,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
|
1,
|
||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
@@ -143,6 +144,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
|
1,
|
||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||||
client.SubClient.ConnectSocketSub(sub1);
|
client.SubClient.ConnectSocketSub(sub1);
|
||||||
client.SubClient.ConnectSocketSub(sub2);
|
client.SubClient.ConnectSocketSub(sub2);
|
||||||
|
var us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
|
||||||
|
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
|
||||||
|
sub1.AddSubscription(us1);
|
||||||
|
sub2.AddSubscription(us2);
|
||||||
|
var ups1 = new UpdateSubscription(sub1, us1);
|
||||||
|
var ups2 = new UpdateSubscription(sub2, us2);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAllAsync().Wait();
|
client.UnsubscribeAllAsync().Wait();
|
||||||
|
|||||||
@@ -60,11 +60,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
headers = new Dictionary<string, string>();
|
headers = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Sign(string toSign)
|
|
||||||
{
|
|
||||||
return toSign;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetKey() => _credentials.Key.GetString();
|
public string GetKey() => _credentials.Key.GetString();
|
||||||
public string GetSecret() => _credentials.Secret.GetString();
|
public string GetSecret() => _credentials.Secret.GetString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,9 +182,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(JToken error)
|
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||||
{
|
{
|
||||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
var errorData = ValidateJson(data);
|
||||||
|
|
||||||
|
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
public override TimeSpan? GetTimeOffset()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public event Action OnReconnected;
|
public event Action OnReconnected;
|
||||||
public event Action OnReconnecting;
|
public event Action OnReconnecting;
|
||||||
#pragma warning restore 0067
|
#pragma warning restore 0067
|
||||||
|
public event Action<int> OnRequestSent;
|
||||||
public event Action<string> OnMessage;
|
public event Action<string> OnMessage;
|
||||||
public event Action<Exception> OnError;
|
public event Action<Exception> OnError;
|
||||||
public event Action OnOpen;
|
public event Action OnOpen;
|
||||||
@@ -69,10 +70,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
return Task.FromResult(CanConnect);
|
return Task.FromResult(CanConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(string data)
|
public void Send(int requestId, string data, int weight)
|
||||||
{
|
{
|
||||||
if(!Connected)
|
if(!Connected)
|
||||||
throw new Exception("Socket not connected");
|
throw new Exception("Socket not connected");
|
||||||
|
OnRequestSent?.Invoke(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
|
|||||||
@@ -74,6 +74,17 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
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>
|
/// <summary>
|
||||||
/// SHA256 sign the data and return the hash
|
/// SHA256 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -87,6 +98,19 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
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>
|
/// <summary>
|
||||||
/// SHA384 sign the data and return the hash
|
/// SHA384 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -100,6 +124,41 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
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>
|
/// <summary>
|
||||||
/// SHA512 sign the data and return the hash
|
/// SHA512 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -113,6 +172,41 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
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>
|
/// <summary>
|
||||||
/// MD5 sign the data and return the hash
|
/// MD5 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -127,28 +221,70 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="data">Data to sign</param>
|
/// <param name="data">Data to sign</param>
|
||||||
/// <param name="outputType">String type</param>
|
/// <param name="outputType">String type</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
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);
|
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);
|
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>
|
/// <summary>
|
||||||
/// HMACSHA384 sign the data and return the hash
|
/// HMACSHA384 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">Data to sign</param>
|
/// <param name="data">Data to sign</param>
|
||||||
/// <param name="outputType">String type</param>
|
/// <param name="outputType">String type</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
using var encryptor = new HMACSHA384(_sBytes);
|
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);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +318,46 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
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 (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||||
{
|
{
|
||||||
#if NETSTANDARD2_1_OR_GREATER
|
#if NETSTANDARD2_1_OR_GREATER
|
||||||
@@ -209,30 +384,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
throw new Exception("Invalid credentials type");
|
throw new Exception("Invalid credentials type");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var sha256 = SHA256.Create();
|
return rsa;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Net.Http;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
@@ -78,22 +79,9 @@ namespace CryptoExchange.Net
|
|||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
/// The default serializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected static int _lastId;
|
protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default);
|
||||||
/// <summary>
|
|
||||||
/// Lock for id generating
|
|
||||||
/// </summary>
|
|
||||||
protected static object _idLock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A default serializer
|
|
||||||
/// </summary>
|
|
||||||
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture
|
|
||||||
});
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Api options
|
||||||
@@ -213,7 +201,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
{
|
{
|
||||||
serializer ??= _defaultSerializer;
|
serializer ??= DefaultSerializer;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -251,7 +239,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
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;
|
string? data = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -338,19 +326,6 @@ namespace CryptoExchange.Net
|
|||||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static int NextId()
|
|
||||||
{
|
|
||||||
lock (_idLock)
|
|
||||||
{
|
|
||||||
_lastId += 1;
|
|
||||||
return _lastId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options"></param>
|
/// <param name="options"></param>
|
||||||
/// <exception cref="ArgumentNullException"></exception>
|
/// <exception cref="ArgumentNullException"></exception>
|
||||||
public virtual void Initialize(ExchangeOptions options)
|
protected virtual void Initialize(ExchangeOptions options)
|
||||||
{
|
{
|
||||||
if (options == null)
|
if (options == null)
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -82,6 +83,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
/// <param name="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="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="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
@@ -96,6 +98,7 @@ namespace CryptoExchange.Net
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, object>? parameters = null,
|
Dictionary<string, object>? parameters = null,
|
||||||
bool signed = false,
|
bool signed = false,
|
||||||
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
@@ -107,11 +110,16 @@ namespace CryptoExchange.Net
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
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)
|
if (!request)
|
||||||
return new WebCallResult(request.Error!);
|
return new WebCallResult(request.Error!);
|
||||||
|
|
||||||
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
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))
|
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -128,6 +136,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
/// <param name="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="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="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
@@ -142,6 +151,7 @@ namespace CryptoExchange.Net
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, object>? parameters = null,
|
Dictionary<string, object>? parameters = null,
|
||||||
bool signed = false,
|
bool signed = false,
|
||||||
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
@@ -154,11 +164,16 @@ namespace CryptoExchange.Net
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
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)
|
if (!request)
|
||||||
return new WebCallResult<T>(request.Error!);
|
return new WebCallResult<T>(request.Error!);
|
||||||
|
|
||||||
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
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))
|
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -174,6 +189,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
/// <param name="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="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="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
@@ -187,6 +203,7 @@ namespace CryptoExchange.Net
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, object>? parameters = null,
|
Dictionary<string, object>? parameters = null,
|
||||||
bool signed = false,
|
bool signed = false,
|
||||||
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
@@ -194,7 +211,7 @@ namespace CryptoExchange.Net
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
bool ignoreRatelimit = false)
|
bool ignoreRatelimit = false)
|
||||||
{
|
{
|
||||||
var requestId = NextId();
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
if (signed)
|
if (signed)
|
||||||
{
|
{
|
||||||
@@ -231,7 +248,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
_logger.Log(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
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 = "";
|
string? paramString = "";
|
||||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||||
@@ -260,9 +277,9 @@ namespace CryptoExchange.Net
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
bool expectedEmptyResponse)
|
bool expectedEmptyResponse)
|
||||||
{
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
var statusCode = response.StatusCode;
|
var statusCode = response.StatusCode;
|
||||||
@@ -272,7 +289,7 @@ namespace CryptoExchange.Net
|
|||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
// 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)
|
if (manualParseError)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(responseStream);
|
using var reader = new StreamReader(responseStream);
|
||||||
@@ -280,23 +297,22 @@ namespace CryptoExchange.Net
|
|||||||
responseLength ??= data.Length;
|
responseLength ??= data.Length;
|
||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.Close();
|
response.Close();
|
||||||
_logger.Log(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(OutputOriginalData ? (": " + data) : "")}");
|
|
||||||
|
|
||||||
if (!expectedEmptyResponse)
|
if (!expectedEmptyResponse)
|
||||||
{
|
{
|
||||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||||
var parseResult = ValidateJson(data);
|
var parseResult = ValidateJson(data);
|
||||||
if (!parseResult.Success)
|
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
|
// 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);
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
if (error != null)
|
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
|
// Not an error, so continue deserializing
|
||||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -305,16 +321,16 @@ namespace CryptoExchange.Net
|
|||||||
var parseResult = ValidateJson(data);
|
var parseResult = ValidateJson(data);
|
||||||
if (!parseResult.Success)
|
if (!parseResult.Success)
|
||||||
// Not empty, and not json
|
// 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);
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
if (error != null)
|
if (error != null)
|
||||||
// Error response
|
// 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
|
// 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
|
else
|
||||||
@@ -325,7 +341,7 @@ namespace CryptoExchange.Net
|
|||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.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
|
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||||
@@ -333,7 +349,7 @@ namespace CryptoExchange.Net
|
|||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.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
|
else
|
||||||
@@ -341,36 +357,37 @@ namespace CryptoExchange.Net
|
|||||||
// Http status code indicates error
|
// Http status code indicates error
|
||||||
using var reader = new StreamReader(responseStream);
|
using var reader = new StreamReader(responseStream);
|
||||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
|
||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.Close();
|
response.Close();
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
Error error;
|
||||||
|
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||||
|
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
||||||
|
else
|
||||||
|
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
||||||
|
|
||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
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)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
var exceptionInfo = requestException.ToLogString();
|
||||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + 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));
|
||||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
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"));
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,6 +423,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized</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="requestId">Unique id of a request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -416,6 +434,7 @@ namespace CryptoExchange.Net
|
|||||||
bool signed,
|
bool signed,
|
||||||
HttpMethodParameterPosition parameterPosition,
|
HttpMethodParameterPosition parameterPosition,
|
||||||
ArrayParametersSerialization arraySerialization,
|
ArrayParametersSerialization arraySerialization,
|
||||||
|
RequestBodyFormat bodyFormat,
|
||||||
int requestId,
|
int requestId,
|
||||||
Dictionary<string, string>? additionalHeaders)
|
Dictionary<string, string>? additionalHeaders)
|
||||||
{
|
{
|
||||||
@@ -529,13 +548,39 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The string the request returned</param>
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
|
/// <param name="data">The response data</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Error ParseErrorResponse(JToken error)
|
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||||
{
|
{
|
||||||
return new ServerError(error.ToString());
|
return new ServerError(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
|
/// <param name="data">The response data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||||
|
{
|
||||||
|
// Handle retry after header
|
||||||
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (retryAfterHeader.Value?.Any() != true)
|
||||||
|
return new ServerRateLimitError(data);
|
||||||
|
|
||||||
|
var value = retryAfterHeader.Value.First();
|
||||||
|
if (int.TryParse(value, out var seconds))
|
||||||
|
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||||
|
|
||||||
|
if (DateTime.TryParse(value, out var datetime))
|
||||||
|
return new ServerRateLimitError(data) { RetryAfter = datetime };
|
||||||
|
|
||||||
|
return new ServerRateLimitError(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -548,14 +593,14 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
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 (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
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;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -584,7 +629,7 @@ namespace CryptoExchange.Net
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static CryptoExchange.Net.Objects.RateLimiter;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -76,9 +77,9 @@ namespace CryptoExchange.Net
|
|||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max amount of outgoing messages per socket per second
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
@@ -130,6 +131,10 @@ namespace CryptoExchange.Net
|
|||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
|
var rateLimiters = new List<IRateLimiter>();
|
||||||
|
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||||
|
rateLimiters.Add(rateLimiter);
|
||||||
|
RateLimiters = rateLimiters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -275,7 +280,7 @@ namespace CryptoExchange.Net
|
|||||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||||
{
|
{
|
||||||
CallResult<object>? callResult = null;
|
CallResult<object>? callResult = null;
|
||||||
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, 1, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (callResult?.Success == true)
|
if (callResult?.Success == true)
|
||||||
{
|
{
|
||||||
@@ -295,10 +300,11 @@ namespace CryptoExchange.Net
|
|||||||
/// <typeparam name="T">Expected result type</typeparam>
|
/// <typeparam name="T">Expected result type</typeparam>
|
||||||
/// <param name="request">The request to send, will be serialized to json</param>
|
/// <param name="request">The request to send, will be serialized to json</param>
|
||||||
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
||||||
|
/// <param name="weight">Weight of the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
|
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated, int weight = 1)
|
||||||
{
|
{
|
||||||
return QueryAsync<T>(BaseAddress, request, authenticated);
|
return QueryAsync<T>(BaseAddress, request, authenticated, weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -308,8 +314,9 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="request">The request to send</param>
|
/// <param name="request">The request to send</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <param name="weight">Weight of the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
|
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated, int weight = 1)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
@@ -348,7 +355,7 @@ namespace CryptoExchange.Net
|
|||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
return await QueryAndWaitAsync<T>(socketConnection, request, weight).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -357,11 +364,12 @@ namespace CryptoExchange.Net
|
|||||||
/// <typeparam name="T">The expected result type</typeparam>
|
/// <typeparam name="T">The expected result type</typeparam>
|
||||||
/// <param name="socket">The connection to send and wait on</param>
|
/// <param name="socket">The connection to send and wait on</param>
|
||||||
/// <param name="request">The request to send</param>
|
/// <param name="request">The request to send</param>
|
||||||
|
/// <param name="weight">The weight of the query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request, int weight)
|
||||||
{
|
{
|
||||||
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||||
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, data =>
|
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, weight, data =>
|
||||||
{
|
{
|
||||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||||
return false;
|
return false;
|
||||||
@@ -394,7 +402,7 @@ namespace CryptoExchange.Net
|
|||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult<bool>(true);
|
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);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
@@ -406,6 +414,7 @@ namespace CryptoExchange.Net
|
|||||||
return new CallResult<bool>(result.Error);
|
return new CallResult<bool>(result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} authenticated");
|
||||||
socket.Authenticated = true;
|
socket.Authenticated = true;
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
@@ -503,7 +512,7 @@ namespace CryptoExchange.Net
|
|||||||
if (typeof(T) == typeof(string))
|
if (typeof(T) == typeof(string))
|
||||||
{
|
{
|
||||||
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,12 +523,12 @@ namespace CryptoExchange.Net
|
|||||||
return;
|
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
|
var subscription = request == null
|
||||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
? SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
: SocketSubscription.CreateForRequest(ExchangeHelpers.NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||||
if (!connection.AddSubscription(subscription))
|
if (!connection.AddSubscription(subscription))
|
||||||
return null;
|
return null;
|
||||||
return subscription;
|
return subscription;
|
||||||
@@ -533,7 +542,7 @@ namespace CryptoExchange.Net
|
|||||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
||||||
{
|
{
|
||||||
genericHandlers.Add(identifier, action);
|
genericHandlers.Add(identifier, action);
|
||||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
|
var subscription = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier, false, false, action);
|
||||||
foreach (var connection in socketConnections.Values)
|
foreach (var connection in socketConnections.Values)
|
||||||
connection.AddSubscription(subscription);
|
connection.AddSubscription(subscription);
|
||||||
}
|
}
|
||||||
@@ -554,7 +563,7 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||||
{
|
{
|
||||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||||
}
|
}
|
||||||
@@ -564,7 +573,7 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The original request</param>
|
/// <param name="request">The original request</param>
|
||||||
/// <returns></returns>
|
/// <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));
|
return Task.FromResult(new CallResult<object>(request));
|
||||||
}
|
}
|
||||||
@@ -607,7 +616,7 @@ namespace CryptoExchange.Net
|
|||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
foreach (var kvp in genericHandlers)
|
foreach (var kvp in genericHandlers)
|
||||||
{
|
{
|
||||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
|
var handler = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), kvp.Key, false, false, kvp.Value);
|
||||||
socketConnection.AddSubscription(handler);
|
socketConnection.AddSubscription(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +660,7 @@ namespace CryptoExchange.Net
|
|||||||
DataInterpreterString = dataInterpreterString,
|
DataInterpreterString = dataInterpreterString,
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
RateLimiters = RateLimiters,
|
||||||
Proxy = ClientOptions.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||||
};
|
};
|
||||||
@@ -674,7 +683,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="identifier">Identifier for the periodic send</param>
|
/// <param name="identifier">Identifier for the periodic send</param>
|
||||||
/// <param name="interval">How often</param>
|
/// <param name="interval">How often</param>
|
||||||
/// <param name="objGetter">Method returning the object to send</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)
|
if (objGetter == null)
|
||||||
throw new ArgumentNullException(nameof(objGetter));
|
throw new ArgumentNullException(nameof(objGetter));
|
||||||
@@ -704,7 +713,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
socketConnection.Send(obj);
|
socketConnection.Send(ExchangeHelpers.NextId(), obj, 1);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -762,6 +771,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
|
var sum = socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||||
|
if (sum == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,13 +109,21 @@ namespace CryptoExchange.Net.Converters
|
|||||||
{
|
{
|
||||||
if (token.Type == JTokenType.Null)
|
if (token.Type == JTokenType.Null)
|
||||||
value = 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?))
|
|| property.PropertyType == typeof(decimal?))
|
||||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
&& (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);
|
property.SetValue(result, dec);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
switch (reader.Value?.ToString().ToLower().Trim())
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
/// </summary>
|
||||||
public class EnumConverter : JsonConverter
|
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();
|
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type objectType)
|
public override bool CanConvert(Type objectType)
|
||||||
{
|
{
|
||||||
return objectType.IsEnum;
|
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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");
|
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
|
else
|
||||||
|
{
|
||||||
// We received an enum value but weren't able to parse it.
|
// 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;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,22 +138,39 @@ namespace CryptoExchange.Net.Converters
|
|||||||
/// <param name="enumValue"></param>
|
/// <param name="enumValue"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
[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;
|
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||||
|
|
||||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||||
mapping = AddMapping(objectType);
|
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 />
|
/// <inheritdoc />
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var stringValue = GetString(value);
|
if (value == null)
|
||||||
writer.WriteValue(stringValue);
|
{
|
||||||
|
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>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||||
<PackageVersion>6.0.3</PackageVersion>
|
<PackageVersion>6.2.2</PackageVersion>
|
||||||
<AssemblyVersion>6.0.3</AssemblyVersion>
|
<AssemblyVersion>6.2.2</AssemblyVersion>
|
||||||
<FileVersion>6.0.3</FileVersion>
|
<FileVersion>6.2.2</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>6.0.3 - Fixed Proxy not getting applied in rest clients when not using DI</PackageReleaseNotes>
|
<PackageReleaseNotes>6.2.2 - Added support for specifying the request body content type on a per request basis, Added DecimalStringWriterConverter, Added RequestId to WebCallResult model, Updated response logging</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -8,6 +9,17 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ExchangeHelpers
|
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>
|
||||||
|
private static int _lastId;
|
||||||
|
/// <summary>
|
||||||
|
/// Lock for id generating
|
||||||
|
/// </summary>
|
||||||
|
private static object _idLock = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a value between a min and max
|
/// Clamp a value between a min and max
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -118,5 +130,56 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
return value / 1.000000000000000000000000000000000m;
|
return value / 1.000000000000000000000000000000000m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static int NextId()
|
||||||
|
{
|
||||||
|
lock (_idLock)
|
||||||
|
{
|
||||||
|
_lastId += 1;
|
||||||
|
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
|
/// Total amount of requests made with this API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int TotalRequestsMade { get; set; }
|
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
|
/// The factory for creating sockets. Used for unit testing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IWebsocketFactory SocketFactory { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -45,19 +37,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task ReconnectAsync();
|
Task ReconnectAsync();
|
||||||
/// <summary>
|
/// <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
|
/// Unsubscribe all subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
|||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Webscoket connection interface
|
/// Websocket connection interface
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IWebsocket: IDisposable
|
public interface IWebsocket: IDisposable
|
||||||
{
|
{
|
||||||
@@ -21,6 +21,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<string> OnMessage;
|
event Action<string> OnMessage;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Websocket sent event, RequestId as parameter
|
||||||
|
/// </summary>
|
||||||
|
event Action<int> OnRequestSent;
|
||||||
|
/// <summary>
|
||||||
/// Websocket error event
|
/// Websocket error event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<Exception> OnError;
|
event Action<Exception> OnError;
|
||||||
@@ -69,8 +73,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data
|
/// Send data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
void Send(string data);
|
/// <param name="weight"></param>
|
||||||
|
void Send(int id, string data, int weight);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reconnect the socket
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request id
|
||||||
|
/// </summary>
|
||||||
|
public int? RequestId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The url which was requested
|
/// The url which was requested
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -217,6 +222,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="code"></param>
|
/// <param name="code"></param>
|
||||||
/// <param name="responseHeaders"></param>
|
/// <param name="responseHeaders"></param>
|
||||||
/// <param name="responseTime"></param>
|
/// <param name="responseTime"></param>
|
||||||
|
/// <param name="requestId"></param>
|
||||||
/// <param name="requestUrl"></param>
|
/// <param name="requestUrl"></param>
|
||||||
/// <param name="requestBody"></param>
|
/// <param name="requestBody"></param>
|
||||||
/// <param name="requestMethod"></param>
|
/// <param name="requestMethod"></param>
|
||||||
@@ -226,6 +232,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
|
int? requestId,
|
||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
@@ -235,6 +242,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
|
RequestId = requestId;
|
||||||
|
|
||||||
RequestUrl = requestUrl;
|
RequestUrl = requestUrl;
|
||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
@@ -255,7 +263,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult AsError(Error error)
|
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 />
|
/// <inheritdoc />
|
||||||
@@ -281,6 +289,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request id
|
||||||
|
/// </summary>
|
||||||
|
public int? RequestId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The url which was requested
|
/// The url which was requested
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -319,6 +332,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="responseTime"></param>
|
/// <param name="responseTime"></param>
|
||||||
/// <param name="responseLength"></param>
|
/// <param name="responseLength"></param>
|
||||||
/// <param name="originalData"></param>
|
/// <param name="originalData"></param>
|
||||||
|
/// <param name="requestId"></param>
|
||||||
/// <param name="requestUrl"></param>
|
/// <param name="requestUrl"></param>
|
||||||
/// <param name="requestBody"></param>
|
/// <param name="requestBody"></param>
|
||||||
/// <param name="requestMethod"></param>
|
/// <param name="requestMethod"></param>
|
||||||
@@ -331,6 +345,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
string? originalData,
|
string? originalData,
|
||||||
|
int? requestId,
|
||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
@@ -343,6 +358,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
ResponseLength = responseLength;
|
ResponseLength = responseLength;
|
||||||
|
|
||||||
|
RequestId = requestId;
|
||||||
RequestUrl = requestUrl;
|
RequestUrl = requestUrl;
|
||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
RequestHeaders = requestHeaders;
|
RequestHeaders = requestHeaders;
|
||||||
@@ -355,7 +371,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDataless()
|
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>
|
/// <summary>
|
||||||
/// Copy as a dataless result
|
/// Copy as a dataless result
|
||||||
@@ -363,14 +379,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDatalessError(Error error)
|
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>
|
/// <summary>
|
||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <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>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -380,7 +396,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
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>
|
/// <summary>
|
||||||
@@ -391,7 +407,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
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 />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -124,4 +124,19 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Closest
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace CryptoExchange.Net.Objects
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for errors
|
/// Base class for errors
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{Code}: {Message} {Data}";
|
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +54,14 @@
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CantConnectError() : base(null, "Can't connect to the server", null) { }
|
public CantConnectError() : base(null, "Can't connect to the server", null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -63,12 +73,20 @@
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
|
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error returned by the server
|
/// Error returned by the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ServerError: Error
|
public class ServerError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -83,9 +101,15 @@
|
|||||||
/// <param name="code"></param>
|
/// <param name="code"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
public ServerError(int code, string message, object? data = null) : base(code, message, data)
|
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||||
{
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -107,6 +131,14 @@
|
|||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected WebError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -120,6 +152,14 @@
|
|||||||
/// <param name="message">The error message</param>
|
/// <param name="message">The error message</param>
|
||||||
/// <param name="data">The data which caused the error</param>
|
/// <param name="data">The data which caused the error</param>
|
||||||
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,6 +173,14 @@
|
|||||||
/// <param name="message">Error message</param>
|
/// <param name="message">Error message</param>
|
||||||
/// <param name="data">Error data</param>
|
/// <param name="data">Error data</param>
|
||||||
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -145,18 +193,73 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit exceeded
|
/// Rate limit exceeded (client side)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RateLimitError: Error
|
public abstract class BaseRateLimitError : Error
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When the request can be retried
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RetryAfter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit exceeded (client side)
|
||||||
|
/// </summary>
|
||||||
|
public class ClientRateLimitError : BaseRateLimitError
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { }
|
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit exceeded (server side)
|
||||||
|
/// </summary>
|
||||||
|
public class ServerRateLimitError : BaseRateLimitError
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -168,17 +271,33 @@
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid operation requested
|
/// Invalid operation requested
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class InvalidOperationError: Error
|
public class InvalidOperationError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public InvalidOperationError(string message) : base(null, message, null) { }
|
public InvalidOperationError(string message) : base(null, message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
@@ -8,6 +10,11 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SocketApiOptions : ApiOptions
|
public class SocketApiOptions : ApiOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of rate limiters to use
|
||||||
|
/// </summary>
|
||||||
|
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||||
/// for example when the server sends intermittent ping requests
|
/// for example when the server sends intermittent ping requests
|
||||||
@@ -30,6 +37,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
|
RateLimiters = RateLimiters,
|
||||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||||
MaxSocketConnections = MaxSocketConnections,
|
MaxSocketConnections = MaxSocketConnections,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class RateLimiter : IRateLimiter
|
public class RateLimiter : IRateLimiter
|
||||||
{
|
{
|
||||||
private readonly object _limiterLock = new object();
|
private readonly object _limiterLock = new object();
|
||||||
internal List<Limiter> Limiters = new List<Limiter>();
|
internal List<Limiter> _limiters = new List<Limiter>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
||||||
@@ -35,7 +35,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
_limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,20 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
_limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a rate limit for the amount of messages that can be send per connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endpoint">The endpoint that the limit is for</param>
|
||||||
|
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||||
|
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||||
|
public RateLimiter AddConnectionRateLimit(string endpoint, int limit, TimeSpan perTimePeriod)
|
||||||
|
{
|
||||||
|
lock (_limiterLock)
|
||||||
|
_limiters.Add(new ConnectionRateLimiter(new[] { endpoint }, limit, perTimePeriod));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +117,10 @@ namespace CryptoExchange.Net.Objects
|
|||||||
{
|
{
|
||||||
int totalWaitTime = 0;
|
int totalWaitTime = 0;
|
||||||
|
|
||||||
EndpointRateLimiter? endpointLimit;
|
List<EndpointRateLimiter> endpointLimits;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
|
||||||
if(endpointLimit != null)
|
foreach (var endpointLimit in endpointLimits)
|
||||||
{
|
{
|
||||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||||
if (!waitResult)
|
if (!waitResult)
|
||||||
@@ -116,12 +129,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
totalWaitTime += waitResult.Data;
|
totalWaitTime += waitResult.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpointLimit?.IgnoreOtherRateLimits == true)
|
if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
|
||||||
return new CallResult<int>(totalWaitTime);
|
return new CallResult<int>(totalWaitTime);
|
||||||
|
|
||||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
partialEndpointLimits = _limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
||||||
foreach (var partialEndpointLimit in partialEndpointLimits)
|
foreach (var partialEndpointLimit in partialEndpointLimits)
|
||||||
{
|
{
|
||||||
if (partialEndpointLimit.CountPerEndpoint)
|
if (partialEndpointLimit.CountPerEndpoint)
|
||||||
@@ -129,11 +142,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
SingleTopicRateLimiter? thisEndpointLimit;
|
SingleTopicRateLimiter? thisEndpointLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
{
|
{
|
||||||
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
thisEndpointLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
||||||
if (thisEndpointLimit == null)
|
if (thisEndpointLimit == null)
|
||||||
{
|
{
|
||||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
||||||
Limiters.Add(thisEndpointLimit);
|
_limiters.Add(thisEndpointLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +169,10 @@ namespace CryptoExchange.Net.Objects
|
|||||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
||||||
return new CallResult<int>(totalWaitTime);
|
return new CallResult<int>(totalWaitTime);
|
||||||
|
|
||||||
ApiKeyRateLimiter? apiLimit;
|
List<ApiKeyRateLimiter> apiLimits;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
|
||||||
if (apiLimit != null)
|
foreach (var apiLimit in apiLimits)
|
||||||
{
|
{
|
||||||
if(apiKey == null)
|
if(apiKey == null)
|
||||||
{
|
{
|
||||||
@@ -177,11 +190,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
SingleTopicRateLimiter? thisApiLimit;
|
SingleTopicRateLimiter? thisApiLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
{
|
{
|
||||||
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
thisApiLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
||||||
if (thisApiLimit == null)
|
if (thisApiLimit == null)
|
||||||
{
|
{
|
||||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
||||||
Limiters.Add(thisApiLimit);
|
_limiters.Add(thisApiLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,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);
|
return new CallResult<int>(totalWaitTime);
|
||||||
|
|
||||||
TotalRateLimiter? totalLimit;
|
List<TotalRateLimiter> totalLimits;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
|
||||||
if (totalLimit != null)
|
foreach(var totalLimit in totalLimits)
|
||||||
{
|
{
|
||||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||||
if (!waitResult)
|
if (!waitResult)
|
||||||
@@ -224,63 +237,68 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
|
|
||||||
int totalWaitTime = 0;
|
try
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
// Remove requests no longer in time period from the history
|
int totalWaitTime = 0;
|
||||||
var checkTime = DateTime.UtcNow;
|
while (true)
|
||||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
|
||||||
{
|
{
|
||||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
// Remove requests no longer in time period from the history
|
||||||
|
var checkTime = DateTime.UtcNow;
|
||||||
|
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||||
{
|
{
|
||||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||||
i--;
|
{
|
||||||
|
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
|
||||||
|
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||||
|
{
|
||||||
|
if (currentWeight == 0)
|
||||||
|
throw new Exception("Request limit reached without any prior request. " +
|
||||||
|
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
||||||
|
|
||||||
|
// Wait until the next entry should be removed from the history
|
||||||
|
var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
|
||||||
|
if (thisWaitTime > 0)
|
||||||
|
{
|
||||||
|
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||||
|
{
|
||||||
|
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||||
|
logger.Log(LogLevel.Warning, msg);
|
||||||
|
return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return new CallResult<int>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
totalWaitTime += thisWaitTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
|
|
||||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
|
||||||
{
|
|
||||||
if (currentWeight == 0)
|
|
||||||
throw new Exception("Request limit reached without any prior request. " +
|
|
||||||
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
|
||||||
|
|
||||||
// Wait until the next entry should be removed from the history
|
|
||||||
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
|
|
||||||
if (thisWaitTime > 0)
|
|
||||||
{
|
{
|
||||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
break;
|
||||||
{
|
|
||||||
historyTopic.Semaphore.Release();
|
|
||||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
|
||||||
logger.Log(LogLevel.Warning, msg);
|
|
||||||
return new CallResult<int>(new RateLimitError(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return new CallResult<int>(new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
totalWaitTime += thisWaitTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newTime = DateTime.UtcNow;
|
var newTime = DateTime.UtcNow;
|
||||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||||
historyTopic.Semaphore.Release();
|
return new CallResult<int>(totalWaitTime);
|
||||||
return new CallResult<int>(totalWaitTime);
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
historyTopic.Semaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal struct LimitEntry
|
internal struct LimitEntry
|
||||||
@@ -329,6 +347,24 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class ConnectionRateLimiter : PartialEndpointRateLimiter
|
||||||
|
{
|
||||||
|
public ConnectionRateLimiter(int limit, TimeSpan perPeriod)
|
||||||
|
: base(new[] { "/" }, limit, perPeriod, null, true, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConnectionRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod)
|
||||||
|
: base(endpoints, limit, perPeriod, null, true, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return nameof(ConnectionRateLimiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class EndpointRateLimiter: Limiter
|
internal class EndpointRateLimiter: Limiter
|
||||||
{
|
{
|
||||||
public string[] Endpoints { get; set; }
|
public string[] Endpoints { get; set; }
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
var pbList = _processBuffer.ToList();
|
var pbList = _processBuffer.ToList();
|
||||||
if (pbList.Count > 0)
|
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)
|
foreach (var bufferEntry in pbList)
|
||||||
{
|
{
|
||||||
@@ -661,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
if (_stopProcessing)
|
if (_stopProcessing)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Trace, "Skipping message because of resubscribing");
|
_logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -30,9 +31,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
private static readonly object _streamIdLock = new();
|
private static readonly object _streamIdLock = new();
|
||||||
|
|
||||||
private readonly AsyncResetEvent _sendEvent;
|
private readonly AsyncResetEvent _sendEvent;
|
||||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
private readonly ConcurrentQueue<SendItem> _sendBuffer;
|
||||||
private readonly SemaphoreSlim _closeSem;
|
private readonly SemaphoreSlim _closeSem;
|
||||||
private readonly List<DateTime> _outgoingMessages;
|
|
||||||
|
|
||||||
private ClientWebSocket _socket;
|
private ClientWebSocket _socket;
|
||||||
private CancellationTokenSource _ctsSource;
|
private CancellationTokenSource _ctsSource;
|
||||||
@@ -103,6 +103,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<string>? OnMessage;
|
public event Action<string>? OnMessage;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event Action<int>? OnRequestSent;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<Exception>? OnError;
|
public event Action<Exception>? OnError;
|
||||||
|
|
||||||
@@ -128,10 +131,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
Parameters = websocketParameters;
|
Parameters = websocketParameters;
|
||||||
_outgoingMessages = new List<DateTime>();
|
|
||||||
_receivedMessages = new List<ReceiveItem>();
|
_receivedMessages = new List<ReceiveItem>();
|
||||||
_sendEvent = new AsyncResetEvent();
|
_sendEvent = new AsyncResetEvent();
|
||||||
_sendBuffer = new ConcurrentQueue<byte[]>();
|
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
_receivedMessagesLock = new object();
|
_receivedMessagesLock = new object();
|
||||||
|
|
||||||
@@ -270,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(string data)
|
public virtual void Send(int id, string data, int weight)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var bytes = Parameters.Encoding.GetBytes(data);
|
var bytes = Parameters.Encoding.GetBytes(data);
|
||||||
_logger.Log(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} bytes to send buffer");
|
||||||
_sendBuffer.Enqueue(bytes);
|
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var limitKey = Uri.ToString() + "/" + Id.ToString();
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
@@ -404,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
while (_sendBuffer.TryDequeue(out var data))
|
while (_sendBuffer.TryDequeue(out var data))
|
||||||
{
|
{
|
||||||
if (Parameters.RatelimitPerSecond != null)
|
if (Parameters.RateLimiters != null)
|
||||||
{
|
{
|
||||||
// Wait for rate limit
|
foreach(var ratelimiter in Parameters.RateLimiters)
|
||||||
DateTime? start = null;
|
|
||||||
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
|
|
||||||
{
|
{
|
||||||
start ??= DateTime.UtcNow;
|
var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
if (limitResult.Success)
|
||||||
|
{
|
||||||
|
if (limitResult.Data > 0)
|
||||||
|
_logger.Log(LogLevel.Debug, $"Socket {Id} - msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start != null)
|
|
||||||
_logger.Log(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||||
_outgoingMessages.Add(DateTime.UtcNow);
|
OnRequestSent?.Invoke(data.Id);
|
||||||
_logger.Log(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {data.Id} - sent {data.Bytes.Length} bytes");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -630,42 +632,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnMessage event
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected void TriggerOnMessage(string data)
|
|
||||||
{
|
|
||||||
LastActionTime = DateTime.UtcNow;
|
|
||||||
OnMessage?.Invoke(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnError event
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ex"></param>
|
|
||||||
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnError event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnOpen() => OnOpen?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnError event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnClose() => OnClose?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnReconnecting event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnReconnected event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if there is no data received for a period longer than the specified timeout
|
/// Checks if there is no data received for a period longer than the specified timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -721,13 +687,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int MessagesSentLastSecond()
|
|
||||||
{
|
|
||||||
var testTime = DateTime.UtcNow;
|
|
||||||
_outgoingMessages.RemoveAll(r => testTime - r > TimeSpan.FromSeconds(1));
|
|
||||||
return _outgoingMessages.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the received messages list, removing messages received longer than 3s ago
|
/// Update the received messages list, removing messages received longer than 3s ago
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -769,6 +728,32 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message info
|
||||||
|
/// </summary>
|
||||||
|
public struct SendItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The request id
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request id
|
||||||
|
/// </summary>
|
||||||
|
public int Weight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp the request was sent
|
||||||
|
/// </summary>
|
||||||
|
public DateTime SendTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The bytes to send
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Bytes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received message info
|
/// Received message info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
@@ -23,35 +24,23 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? OriginalData { get; set; }
|
public string? OriginalData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of update
|
||||||
|
/// </summary>
|
||||||
|
public SocketUpdateType? UpdateType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The received data deserialized into an object
|
/// The received data deserialized into an object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Topic = topic;
|
Topic = topic;
|
||||||
OriginalData = originalData;
|
OriginalData = originalData;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
|
UpdateType = updateType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,7 +51,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data)
|
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>
|
/// <summary>
|
||||||
@@ -74,7 +63,20 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data, string? topic)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
internal class PendingRequest
|
internal class PendingRequest
|
||||||
{
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
public Func<JToken, bool> Handler { get; }
|
public Func<JToken, bool> Handler { get; }
|
||||||
public JToken? Result { get; private set; }
|
public JToken? Result { get; private set; }
|
||||||
public bool Completed { get; private set; }
|
public bool Completed { get; private set; }
|
||||||
@@ -15,17 +16,22 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public TimeSpan Timeout { get; }
|
public TimeSpan Timeout { get; }
|
||||||
public SocketSubscription? Subscription { get; }
|
public SocketSubscription? Subscription { get; }
|
||||||
|
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
|
public PendingRequest(int id, Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
|
||||||
{
|
{
|
||||||
|
Id = id;
|
||||||
Handler = handler;
|
Handler = handler;
|
||||||
Event = new AsyncResetEvent(false, false);
|
Event = new AsyncResetEvent(false, false);
|
||||||
Timeout = timeout;
|
Timeout = timeout;
|
||||||
RequestTimestamp = DateTime.UtcNow;
|
RequestTimestamp = DateTime.UtcNow;
|
||||||
Subscription = subscription;
|
Subscription = subscription;
|
||||||
|
}
|
||||||
|
|
||||||
_cts = new CancellationTokenSource(timeout);
|
public void IsSend()
|
||||||
|
{
|
||||||
|
// Start timeout countdown
|
||||||
|
_cts = new CancellationTokenSource(Timeout);
|
||||||
_cts.Token.Register(Fail, false);
|
_cts.Token.Register(Fail, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
_socket.OnMessage += HandleMessage;
|
_socket.OnMessage += HandleMessage;
|
||||||
|
_socket.OnRequestSent += HandleRequestSent;
|
||||||
_socket.OnOpen += HandleOpen;
|
_socket.OnOpen += HandleOpen;
|
||||||
_socket.OnClose += HandleClose;
|
_socket.OnClose += HandleClose;
|
||||||
_socket.OnReconnecting += HandleReconnecting;
|
_socket.OnReconnecting += HandleReconnecting;
|
||||||
@@ -258,7 +259,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||||
if (!reconnectSuccessful)
|
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);
|
await _socket.ReconnectAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -284,6 +285,25 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for whenever a request is sent over the websocket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestId">Id of the request sent</param>
|
||||||
|
protected virtual void HandleRequestSent(int 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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRequest.IsSend();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process a message received by the socket
|
/// Process a message received by the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -318,7 +338,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Check if this message is an answer on any pending requests
|
// Check if this message is an answer on any pending requests
|
||||||
foreach (var pendingRequest in requests)
|
foreach (var pendingRequest in requests)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (pendingRequest.CheckData(tokenData))
|
if (pendingRequest.CheckData(tokenData))
|
||||||
{
|
{
|
||||||
lock (_pendingRequests)
|
lock (_pendingRequests)
|
||||||
@@ -329,12 +348,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Answer to a timed out request, unsub if it is a subscription request
|
// Answer to a timed out request, unsub if it is a subscription request
|
||||||
if (pendingRequest.Subscription != null)
|
if (pendingRequest.Subscription != null)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout");
|
_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);
|
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {pendingRequest.Id} - received data matched to pending request");
|
||||||
pendingRequest.Succeed(tokenData);
|
pendingRequest.Succeed(tokenData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +383,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
"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>
|
/// <summary>
|
||||||
@@ -570,45 +590,69 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="timeout">The timeout for response</param>
|
/// <param name="timeout">The timeout for response</param>
|
||||||
/// <param name="subscription">Subscription if this is a subscribe request</param>
|
/// <param name="subscription">Subscription if this is a subscribe request</param>
|
||||||
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
|
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
|
||||||
|
/// <param name="weight">The weight of the message</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, Func<JToken, bool> handler)
|
public virtual async Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, int weight, Func<JToken, bool> handler)
|
||||||
{
|
{
|
||||||
var pending = new PendingRequest(handler, timeout, subscription);
|
var pending = new PendingRequest(ExchangeHelpers.NextId(), handler, timeout, subscription);
|
||||||
lock (_pendingRequests)
|
lock (_pendingRequests)
|
||||||
{
|
{
|
||||||
_pendingRequests.Add(pending);
|
_pendingRequests.Add(pending);
|
||||||
}
|
}
|
||||||
var sendOk = Send(obj);
|
|
||||||
if(!sendOk)
|
|
||||||
pending.Fail();
|
|
||||||
|
|
||||||
return pending.Event.WaitAsync(timeout);
|
var sendOk = Send(pending.Id, obj, weight);
|
||||||
|
if (!sendOk)
|
||||||
|
{
|
||||||
|
pending.Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if(!_socket.IsOpen)
|
||||||
|
{
|
||||||
|
pending.Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.Completed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await pending.Event.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (pending.Completed)
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data over the websocket connection
|
/// Send data over the websocket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the object to send</typeparam>
|
/// <typeparam name="T">The type of the object to send</typeparam>
|
||||||
|
/// <param name="requestId">The request id</param>
|
||||||
/// <param name="obj">The object to send</param>
|
/// <param name="obj">The object to send</param>
|
||||||
/// <param name="nullValueHandling">How null values should be serialized</param>
|
/// <param name="nullValueHandling">How null values should be serialized</param>
|
||||||
public virtual bool Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
/// <param name="weight">The weight of the message</param>
|
||||||
|
public virtual bool Send<T>(int requestId, T obj, int weight, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
||||||
{
|
{
|
||||||
if(obj is string str)
|
if(obj is string str)
|
||||||
return Send(str);
|
return Send(requestId, str, weight);
|
||||||
else
|
else
|
||||||
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
return Send(requestId, JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }), weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send string data over the websocket connection
|
/// Send string data over the websocket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The data to send</param>
|
/// <param name="data">The data to send</param>
|
||||||
public virtual bool Send(string data)
|
/// <param name="weight">The weight of the message</param>
|
||||||
|
/// <param name="requestId">The id of the request</param>
|
||||||
|
public virtual bool Send(int requestId, string data, int weight)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
|
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {requestId} - sending messsage: {data}");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_socket.Send(data);
|
_socket.Send(requestId, data, weight);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch(Exception)
|
catch(Exception)
|
||||||
@@ -670,7 +714,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
var result = await ApiClient.RevitalizeRequestAsync(subscription.Request!).ConfigureAwait(false);
|
var result = await ApiClient.RevitalizeRequestAsync(subscription.Request!).ConfigureAwait(false);
|
||||||
if (!result)
|
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);
|
return result.As<bool>(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -52,9 +53,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public TimeSpan? KeepAliveInterval { get; set; }
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max amount of messages to send per second
|
/// The rate limiters for the socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? RatelimitPerSecond { get; set; }
|
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Origin header value to send in the connection handshake
|
/// Origin header value to send in the connection handshake
|
||||||
|
|||||||
@@ -5,14 +5,16 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
<PackageReference Include="Binance.Net" Version="9.1.5" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
|
||||||
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
|
||||||
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
<PackageReference Include="Bybit.Net" Version="3.2.1" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
|
||||||
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
<PackageReference Include="Huobi.Net" Version="5.0.3" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="5.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" />
|
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IBinanceRestClient binanceClient
|
@inject IBinanceRestClient binanceClient
|
||||||
@inject IBitfinexRestClient bitfinexClient
|
@inject IBitfinexRestClient bitfinexClient
|
||||||
|
@inject IBitgetRestClient bitgetClient
|
||||||
@inject IBittrexRestClient bittrexClient
|
@inject IBittrexRestClient bittrexClient
|
||||||
@inject IBybitRestClient bybitClient
|
@inject IBybitRestClient bybitClient
|
||||||
@inject ICoinExRestClient coinexClient
|
@inject ICoinExRestClient coinexClient
|
||||||
@inject IHuobiRestClient huobiClient
|
@inject IHuobiRestClient huobiClient
|
||||||
@inject IKrakenRestClient krakenClient
|
@inject IKrakenRestClient krakenClient
|
||||||
@inject IKucoinRestClient kucoinClient
|
@inject IKucoinRestClient kucoinClient
|
||||||
|
@inject IOKXRestClient okxClient
|
||||||
|
|
||||||
<h3>BTC-USD prices:</h3>
|
<h3>BTC-USD prices:</h3>
|
||||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||||
@@ -21,12 +23,14 @@
|
|||||||
{
|
{
|
||||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||||
|
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
|
||||||
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||||
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
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);
|
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
|
||||||
|
|
||||||
@@ -36,6 +40,9 @@
|
|||||||
if (bitfinexTask.Result.Success)
|
if (bitfinexTask.Result.Success)
|
||||||
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
|
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
|
||||||
|
|
||||||
|
if (bitgetTask.Result.Success)
|
||||||
|
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
|
||||||
|
|
||||||
if (bittrexTask.Result.Success)
|
if (bittrexTask.Result.Success)
|
||||||
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
||||||
|
|
||||||
@@ -53,6 +60,9 @@
|
|||||||
|
|
||||||
if (kucoinTask.Result.Success)
|
if (kucoinTask.Result.Success)
|
||||||
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
|
_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"
|
@page "/LiveData"
|
||||||
@inject IBinanceSocketClient binanceSocketClient
|
@inject IBinanceSocketClient binanceSocketClient
|
||||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||||
|
@inject IBitgetSocketClient bitgetSocketClient
|
||||||
@inject IBittrexSocketClient bittrexSocketClient
|
@inject IBittrexSocketClient bittrexSocketClient
|
||||||
@inject IBybitSocketClient bybitSocketClient
|
@inject IBybitSocketClient bybitSocketClient
|
||||||
@inject ICoinExSocketClient coinExSocketClient
|
@inject ICoinExSocketClient coinExSocketClient
|
||||||
@inject IHuobiSocketClient huobiSocketClient
|
@inject IHuobiSocketClient huobiSocketClient
|
||||||
@inject IKrakenSocketClient krakenSocketClient
|
@inject IKrakenSocketClient krakenSocketClient
|
||||||
@inject IKucoinSocketClient kucoinSocketClient
|
@inject IKucoinSocketClient kucoinSocketClient
|
||||||
|
@inject IOKXSocketClient okxSocketClient
|
||||||
@using System.Collections.Concurrent
|
@using System.Collections.Concurrent
|
||||||
@using CryptoExchange.Net.Objects
|
@using CryptoExchange.Net.Objects
|
||||||
@using CryptoExchange.Net.Sockets
|
@using CryptoExchange.Net.Sockets
|
||||||
@@ -28,12 +30,14 @@
|
|||||||
{
|
{
|
||||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", 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)),
|
bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
||||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||||
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||||
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
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);
|
await Task.WhenAll(tasks);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@using System.Timers
|
@using System.Timers
|
||||||
@using Binance.Net.Interfaces
|
@using Binance.Net.Interfaces
|
||||||
@using Bitfinex.Net.Interfaces
|
@using Bitfinex.Net.Interfaces
|
||||||
|
@using Bitget.Net.Interfaces;
|
||||||
@using Bittrex.Net.Interfaces
|
@using Bittrex.Net.Interfaces
|
||||||
@using Bybit.Net.Interfaces
|
@using Bybit.Net.Interfaces
|
||||||
@using CoinEx.Net.Interfaces
|
@using CoinEx.Net.Interfaces
|
||||||
@@ -11,14 +12,17 @@
|
|||||||
@using Kraken.Net.Interfaces
|
@using Kraken.Net.Interfaces
|
||||||
@using Kucoin.Net.Clients
|
@using Kucoin.Net.Clients
|
||||||
@using Kucoin.Net.Interfaces
|
@using Kucoin.Net.Interfaces
|
||||||
|
@using OKX.Net.Interfaces;
|
||||||
@inject IBinanceOrderBookFactory binanceFactory
|
@inject IBinanceOrderBookFactory binanceFactory
|
||||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||||
|
@inject IBitgetOrderBookFactory bitgetFactory
|
||||||
@inject IBittrexOrderBookFactory bittrexFactory
|
@inject IBittrexOrderBookFactory bittrexFactory
|
||||||
@inject IBybitOrderBookFactory bybitFactory
|
@inject IBybitOrderBookFactory bybitFactory
|
||||||
@inject ICoinExOrderBookFactory coinExFactory
|
@inject ICoinExOrderBookFactory coinExFactory
|
||||||
@inject IHuobiOrderBookFactory huobiFactory
|
@inject IHuobiOrderBookFactory huobiFactory
|
||||||
@inject IKrakenOrderBookFactory krakenFactory
|
@inject IKrakenOrderBookFactory krakenFactory
|
||||||
@inject IKucoinOrderBookFactory kucoinFactory
|
@inject IKucoinOrderBookFactory kucoinFactory
|
||||||
|
@inject IOKXOrderBookFactory okxFactory
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<h3>ETH-BTC books, live updates:</h3>
|
<h3>ETH-BTC books, live updates:</h3>
|
||||||
@@ -54,12 +58,14 @@
|
|||||||
{
|
{
|
||||||
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||||
|
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||||
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
|
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
|
||||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||||
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
|
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
|
||||||
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
||||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||||
|
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||||
};
|
};
|
||||||
|
|
||||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
@page "/SpotClient"
|
@page "/SpotClient"
|
||||||
@inject IBinanceRestClient binanceClient
|
@inject IBinanceRestClient binanceClient
|
||||||
@inject IBitfinexRestClient bitfinexClient
|
@inject IBitfinexRestClient bitfinexClient
|
||||||
|
@inject IBitgetRestClient bitgetClient
|
||||||
@inject IBittrexRestClient bittrexClient
|
@inject IBittrexRestClient bittrexClient
|
||||||
@inject IBybitRestClient bybitClient
|
@inject IBybitRestClient bybitClient
|
||||||
@inject ICoinExRestClient coinexClient
|
@inject ICoinExRestClient coinexClient
|
||||||
@inject IHuobiRestClient huobiClient
|
@inject IHuobiRestClient huobiClient
|
||||||
@inject IKrakenRestClient krakenClient
|
@inject IKrakenRestClient krakenClient
|
||||||
@inject IKucoinRestClient kucoinClient
|
@inject IKucoinRestClient kucoinClient
|
||||||
|
@inject IOKXRestClient okxClient
|
||||||
@using Binance.Net.Clients.SpotApi
|
@using Binance.Net.Clients.SpotApi
|
||||||
@using Bitfinex.Net.Clients.SpotApi
|
@using Bitfinex.Net.Clients.SpotApi
|
||||||
@using Bittrex.Net.Clients.SpotApi
|
@using Bittrex.Net.Clients.SpotApi
|
||||||
|
@using Bitget.Net.Clients.SpotApi
|
||||||
@using Bybit.Net.Clients.SpotApi
|
@using Bybit.Net.Clients.SpotApi
|
||||||
@using CoinEx.Net.Clients.SpotApi
|
@using CoinEx.Net.Clients.SpotApi
|
||||||
@using CryptoExchange.Net.Interfaces
|
@using CryptoExchange.Net.Interfaces
|
||||||
@@ -17,6 +20,7 @@
|
|||||||
@using Huobi.Net.Clients.SpotApi
|
@using Huobi.Net.Clients.SpotApi
|
||||||
@using Kraken.Net.Clients.SpotApi
|
@using Kraken.Net.Clients.SpotApi
|
||||||
@using Kucoin.Net.Clients.SpotApi
|
@using Kucoin.Net.Clients.SpotApi
|
||||||
|
@using OKX.Net.Clients.UnifiedApi
|
||||||
|
|
||||||
<h3>ETH-BTC prices:</h3>
|
<h3>ETH-BTC prices:</h3>
|
||||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||||
@@ -34,12 +38,14 @@
|
|||||||
|
|
||||||
binanceClient.SpotApi.CommonSpotClient,
|
binanceClient.SpotApi.CommonSpotClient,
|
||||||
bitfinexClient.SpotApi.CommonSpotClient,
|
bitfinexClient.SpotApi.CommonSpotClient,
|
||||||
bittrexClient.SpotApi.CommonSpotClient,
|
bitgetClient.SpotApi.CommonSpotClient,
|
||||||
|
bittrexClient.SpotApi.CommonSpotClient,
|
||||||
bybitClient.SpotApiV1.CommonSpotClient,
|
bybitClient.SpotApiV1.CommonSpotClient,
|
||||||
coinexClient.SpotApi.CommonSpotClient,
|
coinexClient.SpotApi.CommonSpotClient,
|
||||||
huobiClient.SpotApi.CommonSpotClient,
|
huobiClient.SpotApi.CommonSpotClient,
|
||||||
krakenClient.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"))));
|
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.Clients;
|
||||||
using Binance.Net.Interfaces.Clients;
|
using Binance.Net.Interfaces.Clients;
|
||||||
using Bitfinex.Net;
|
using Bitfinex.Net;
|
||||||
|
using Bitget.Net;
|
||||||
using Bittrex.Net;
|
using Bittrex.Net;
|
||||||
using Bybit.Net;
|
using Bybit.Net;
|
||||||
using CoinEx.Net;
|
using CoinEx.Net;
|
||||||
@@ -16,6 +17,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OKX.Net;
|
||||||
|
|
||||||
namespace BlazorClient
|
namespace BlazorClient
|
||||||
{
|
{
|
||||||
@@ -48,12 +50,14 @@ namespace BlazorClient
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddBitfinex();
|
services.AddBitfinex();
|
||||||
|
services.AddBitget();
|
||||||
services.AddBittrex();
|
services.AddBittrex();
|
||||||
services.AddBybit();
|
services.AddBybit();
|
||||||
services.AddCoinEx();
|
services.AddCoinEx();
|
||||||
services.AddHuobi();
|
services.AddHuobi();
|
||||||
services.AddKraken();
|
services.AddKraken();
|
||||||
services.AddKucoin();
|
services.AddKucoin();
|
||||||
|
services.AddOKX();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
@using BlazorClient.Shared
|
@using BlazorClient.Shared
|
||||||
@using Binance.Net.Interfaces.Clients;
|
@using Binance.Net.Interfaces.Clients;
|
||||||
@using Bitfinex.Net.Interfaces.Clients;
|
@using Bitfinex.Net.Interfaces.Clients;
|
||||||
|
@using Bitget.Net.Interfaces.Clients;
|
||||||
@using Bittrex.Net.Interfaces.Clients;
|
@using Bittrex.Net.Interfaces.Clients;
|
||||||
@using Bybit.Net.Interfaces.Clients;
|
@using Bybit.Net.Interfaces.Clients;
|
||||||
@using CoinEx.Net.Interfaces.Clients;
|
@using CoinEx.Net.Interfaces.Clients;
|
||||||
@using Huobi.Net.Interfaces.Clients;
|
@using Huobi.Net.Interfaces.Clients;
|
||||||
@using Kraken.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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
<PackageReference Include="Binance.Net" Version="9.1.5" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
|
||||||
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
|
||||||
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
<PackageReference Include="Bybit.Net" Version="3.2.1" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
|
||||||
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
<PackageReference Include="Huobi.Net" Version="5.0.3" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="5.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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# CryptoExchange.Net
|
# 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.
|
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)
|
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||||
[Bybit](https://partner.bybit.com/b/jkorf)
|
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||||
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
[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)
|
[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)
|
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||||
|
|
||||||
@@ -32,6 +31,46 @@ 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).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* 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
|
||||||
|
* Added ServerRatelimitError error
|
||||||
|
|
||||||
* Version 6.0.3 - 23 Jul 2023
|
* Version 6.0.3 - 23 Jul 2023
|
||||||
* Fixed Proxy not getting applied in rest clients when not using DI
|
* Fixed Proxy not getting applied in rest clients when not using DI
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -82,7 +82,7 @@ All clients have access to the following options, specific implementations might
|
|||||||
|
|
||||||
|Option|Description|Default|
|
|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)`
|
|`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`
|
|`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|
|
|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`
|
|`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`|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
|
|`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)**
|
**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>()`|
|
|`RateLimiters`|A list of `IRateLimiter`s to use.|`new List<IRateLimiter>()`|
|
||||||
|`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`|
|
|`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)`
|
|`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)**
|
**Options for Socket Api Client (extension of base api client options)**
|
||||||
|
|||||||
+3
-2
@@ -3,7 +3,7 @@ title: Home
|
|||||||
nav_order: 1
|
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.
|
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,12 +14,14 @@ 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/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/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/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/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/|
|
|<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/|
|
||||||
|<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/|
|
|<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/|
|
||||||
|<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/|
|
|<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/|
|
||||||
|<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/|
|
|<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/|
|
||||||
|
|<a href="https://github.com/JKorf/OKX.Net"><img src="https://raw.githubusercontent.com/JKorf/OKX.Net/358d31f58d8ee51fc234bff1940878a8d0ce5676/Okex.Net/Icon/icon.png"></a>|OKX|https://jkorf.github.io/OKX.Net/|
|
||||||
|
|
||||||
**Implementations by third parties**
|
**Implementations by third parties**
|
||||||
These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version.
|
These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version.
|
||||||
@@ -31,7 +33,6 @@ These might not be compatible with other libraries, make sure to check the Crypt
|
|||||||
|<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex|
|
|<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex|
|
||||||
|<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC|
|
|<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC|
|
||||||
|<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin|
|
|<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin|
|
||||||
|<a href="https://github.com/burakoner/OKEx.Net"><img src="https://github.com/burakoner/OKEx.Net/blob/master/Okex.Net/Icon/icon.png?raw=true"></a>|OKEx|
|
|
||||||
|<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz|
|
|<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz|
|
||||||
|<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk|
|
|<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk|
|
||||||
|<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex|
|
|<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex|
|
||||||
|
|||||||
Reference in New Issue
Block a user