mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 07:56:12 +00:00
Squashed commit of the following:
commit 90f285d7f6bcd926ce9ca3d5832b1d70a5eae6ab Author: JKorf <jankorf91@gmail.com> Date: Sun Jun 25 19:51:12 2023 +0200 Docs commit 72187035c703d1402b37bd2f4c3e066706f28d67 Author: JKorf <jankorf91@gmail.com> Date: Sat Jun 24 16:02:53 2023 +0200 docs commit 8411977292f1fb0b6e0705b1ad675b79a5311d90 Author: JKorf <jankorf91@gmail.com> Date: Fri Jun 23 18:25:15 2023 +0200 wip commit cb7d33aad5d2751104c8b8a6c6eadbf0d36b672c Author: JKorf <jankorf91@gmail.com> Date: Fri Jun 2 19:26:26 2023 +0200 wip commit 4359a2d05ea1141cff516dab18f364a6ca854e18 Author: JKorf <jankorf91@gmail.com> Date: Wed May 31 20:51:36 2023 +0200 wip commit c6adb1b2f728d143f6bd667139c619581122a3c9 Author: JKorf <jankorf91@gmail.com> Date: Mon May 1 21:13:47 2023 +0200 wip commit 7fee733f82fa6ff574030452f0955c9e817647dd Author: JKorf <jankorf91@gmail.com> Date: Thu Apr 27 13:02:56 2023 +0200 wip commit f8057313ffc9b0c31effcda71d35d105ea390971 Author: JKorf <jankorf91@gmail.com> Date: Mon Apr 17 21:37:51 2023 +0200 wip
This commit is contained in:
parent
19cc020852
commit
690f2a63e5
@ -11,66 +11,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture()]
|
||||
public class BaseClientTests
|
||||
{
|
||||
[TestCase]
|
||||
public void SettingLogOutput_Should_RedirectLogOutput()
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new TestOptions()
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
// act
|
||||
client.Log(LogLevel.Information, "Test");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
|
||||
}
|
||||
|
||||
[TestCase(LogLevel.None, LogLevel.Error, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
|
||||
[TestCase(null, LogLevel.Error, true)]
|
||||
[TestCase(null, LogLevel.Warning, true)]
|
||||
[TestCase(null, LogLevel.Information, true)]
|
||||
[TestCase(null, LogLevel.Debug, false)]
|
||||
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var options = new TestOptions()
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
};
|
||||
if (verbosity != null)
|
||||
options.LogLevel = verbosity.Value;
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// act
|
||||
client.Log(testVerbosity, "Test");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
{
|
||||
|
@ -113,6 +113,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
"https://test.com/api",
|
||||
null,
|
||||
@ -140,6 +141,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
"https://test.com/api",
|
||||
null,
|
||||
|
@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
@ -34,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException),
|
||||
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -57,239 +58,96 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestApiOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
BaseAddress = "http://test1.com"
|
||||
},
|
||||
Api2Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("789", "101"),
|
||||
BaseAddress = "http://test2.com"
|
||||
}
|
||||
};
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||
Assert.AreEqual(options.Api2Options.BaseAddress, "http://test2.com");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotOverridenApiOptionsAreStillDefault()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.RateLimitingBehaviour, RateLimitingBehaviour.Wait);
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||
Assert.AreEqual(options.Api2Options.BaseAddress, "https://api2.test.com/");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultBaseOptionsAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.LogLevel, LogLevel.Trace);
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultApiOptionsAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("456", "789")
|
||||
}
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "789");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultApiOptionsWithSomeOverriddenAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("456", "789")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "222");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
var client = new TestRestClient(options => {
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient();
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient(new TestClientOptions
|
||||
var client = new TestRestClient(options =>
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions()
|
||||
{
|
||||
BaseAddress = "http://test.com"
|
||||
}
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "444");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClientOptions: ClientOptions
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </summary>
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions();
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||
{
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
|
||||
public RestApiClientOptions Api1Options
|
||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
{
|
||||
get => _api1Options;
|
||||
set => _api1Options = new RestApiClientOptions(_api1Options, value);
|
||||
}
|
||||
|
||||
private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/");
|
||||
public RestApiClientOptions Api2Options
|
||||
{
|
||||
get => _api2Options;
|
||||
set => _api2Options = new RestApiClientOptions(_api2Options, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions(): this(Default)
|
||||
{
|
||||
}
|
||||
|
||||
public TestClientOptions(TestClientOptions baseOn): base(baseOn)
|
||||
{
|
||||
if (baseOn == null)
|
||||
return;
|
||||
|
||||
ReceiveWindow = baseOn.ReceiveWindow;
|
||||
|
||||
Api1Options = new RestApiClientOptions(baseOn.Api1Options, null);
|
||||
Api2Options = new RestApiClientOptions(baseOn.Api2Options, null);
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@ -106,23 +105,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
|
||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||
}
|
||||
});
|
||||
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
|
||||
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
|
||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@ -136,13 +128,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
}
|
||||
});
|
||||
var client = new TestRestClient();
|
||||
|
||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
||||
|
||||
@ -175,20 +161,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
@ -199,15 +182,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
@ -218,14 +198,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
@ -236,20 +213,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
@ -258,15 +232,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
@ -278,15 +249,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
@ -315,14 +283,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
@ -332,14 +297,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
@ -350,15 +312,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test", true, true, false, true)]
|
||||
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
@ -18,19 +17,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
SubOptions = new SocketApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||
}
|
||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
options.SubOptions.MaxSocketConnections = 1;
|
||||
});
|
||||
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(client.SubClient.Options.ReconnectInterval.TotalSeconds == 6);
|
||||
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@ -43,7 +39,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
socket.CanConnect = canConnect;
|
||||
|
||||
//act
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new Log(""), client.SubClient, socket, null));
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
@ -53,18 +49,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() {
|
||||
SubOptions = new SocketApiClientOptions
|
||||
{
|
||||
ReconnectInterval = TimeSpan.Zero,
|
||||
},
|
||||
LogLevel = LogLevel.Debug
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
@ -87,19 +79,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() {
|
||||
SubOptions = new SocketApiClientOptions
|
||||
{
|
||||
ReconnectInterval = TimeSpan.Zero,
|
||||
OutputOriginalData = enabled
|
||||
},
|
||||
LogLevel = LogLevel.Debug,
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
options.SubOptions.OutputOriginalData = enabled;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
@ -121,17 +109,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
{
|
||||
SubOptions = new SocketApiClientOptions
|
||||
{
|
||||
ReconnectInterval = TimeSpan.Zero,
|
||||
},
|
||||
LogLevel = LogLevel.Debug
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||
var ups = new UpdateSubscription(sub, us);
|
||||
@ -148,20 +131,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
{
|
||||
SubOptions = new SocketApiClientOptions
|
||||
{
|
||||
ReconnectInterval = TimeSpan.Zero,
|
||||
},
|
||||
LogLevel = LogLevel.Debug
|
||||
});
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket1 = client.CreateSocket();
|
||||
var socket2 = client.CreateSocket();
|
||||
socket1.CanConnect = true;
|
||||
socket2.CanConnect = true;
|
||||
var sub1 = new SocketConnection(new Log(""), client.SubClient, socket1, null);
|
||||
var sub2 = new SocketConnection(new Log(""), client.SubClient, socket2, null);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||
client.SubClient.ConnectSocketSub(sub1);
|
||||
client.SubClient.ConnectSocketSub(sub2);
|
||||
|
||||
@ -177,17 +153,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
{
|
||||
SubOptions = new SocketApiClientOptions
|
||||
{
|
||||
ReconnectInterval = TimeSpan.Zero,
|
||||
},
|
||||
LogLevel = LogLevel.Debug
|
||||
});
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub1 = new SocketConnection(new Log(""), client.SubClient, socket, null);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
|
||||
// act
|
||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.OrderBook;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using NUnit.Framework;
|
||||
@ -17,8 +18,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base("Test", "BTC/USD", defaultOrderBookOptions)
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
|
||||
@ -35,12 +37,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
||||
{
|
||||
Status = OrderBookStatus.Synced;
|
||||
base.bids.Clear();
|
||||
base._bids.Clear();
|
||||
foreach (var bid in bids)
|
||||
base.bids.Add(bid.Price, bid);
|
||||
base.asks.Clear();
|
||||
base._bids.Add(bid.Price, bid);
|
||||
base._asks.Clear();
|
||||
foreach (var ask in asks)
|
||||
base.asks.Add(ask.Price, ask);
|
||||
base._asks.Add(ask.Price, ask);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -14,24 +14,28 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public TestSubClient SubClient { get; }
|
||||
|
||||
public TestBaseClient(): base("Test", new TestOptions())
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
SubClient = AddApiClient(new TestSubClient(new TestOptions(), new RestApiClientOptions()));
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(ClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public void Log(LogLevel verbosity, string data)
|
||||
{
|
||||
log.Write(verbosity, data);
|
||||
_logger.Log(verbosity, data);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubClient : RestApiClient
|
||||
{
|
||||
public TestSubClient(ClientOptions options, RestApiClientOptions apiOptions) : base(new Log(""), options, apiOptions)
|
||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@ -60,5 +64,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@ -21,14 +22,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient() : this(new TestClientOptions())
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
{
|
||||
Api1 = new TestRestApi1Client(exchangeOptions);
|
||||
Api2 = new TestRestApi2Client(exchangeOptions);
|
||||
}
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@ -122,7 +131,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
public TestRestApi1Client(TestClientOptions options): base(new Log(""), options, options.Api1Options)
|
||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
@ -163,7 +172,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new Log(""), options, options.Api2Options)
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
@ -197,24 +206,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
}
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
{
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
||||
{
|
||||
uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||
bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -14,34 +16,61 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient() : this(new TestOptions())
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
SubClient = AddApiClient(new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions));
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestOptions: ClientOptions
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public SocketApiClientOptions SubOptions { get; set; } = new SocketApiClientOptions();
|
||||
public string TestAddress { get; }
|
||||
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
||||
{
|
||||
public static TestSocketOptions Default = new TestSocketOptions
|
||||
{
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
|
||||
public TestSubSocketClient(ClientOptions options, SocketApiClientOptions apiOptions): base(new Log(""), options, apiOptions)
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestStringLogger : ILogger
|
||||
{
|
||||
StringBuilder _builder = new StringBuilder();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
_builder.AppendLine(formatter(state, exception));
|
||||
}
|
||||
|
||||
public string GetLogs()
|
||||
{
|
||||
return _builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,6 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
@ -21,16 +21,32 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
public SecureString? Secret { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret)
|
||||
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
@ -40,11 +56,22 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(string key, string secret)
|
||||
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
}
|
||||
@ -56,7 +83,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
21
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
21
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Credentials type
|
||||
/// </summary>
|
||||
public enum ApiCredentialsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hmac keys credentials
|
||||
/// </summary>
|
||||
Hmac,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in xml format
|
||||
/// </summary>
|
||||
RsaXml,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||
/// </summary>
|
||||
RsaPem
|
||||
}
|
||||
}
|
@ -12,14 +12,15 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Base class for authentication providers
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The provided credentials
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
public ApiCredentials Credentials { get; }
|
||||
protected readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
@ -32,7 +33,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
if (credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
Credentials = credentials;
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
}
|
||||
|
||||
@ -83,7 +84,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes): BytesToHexString(resultBytes);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -173,6 +174,47 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!.GetString()
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
|
||||
key)
|
||||
, out _);
|
||||
#else
|
||||
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
|
||||
#endif
|
||||
}
|
||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||
{
|
||||
// Read from xml private key format
|
||||
rsa.FromXmlString(_credentials.Secret!.GetString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// </summary>
|
||||
@ -235,5 +277,26 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -20,14 +21,10 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
private ApiCredentials? _apiCredentials;
|
||||
private AuthenticationProvider? _authenticationProvider;
|
||||
private bool _created;
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected Log _log;
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// If we are disposing
|
||||
@ -37,19 +34,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_created && !_disposing && _apiCredentials != null)
|
||||
{
|
||||
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
|
||||
_created = true;
|
||||
}
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
}
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
@ -83,23 +68,23 @@ namespace CryptoExchange.Net
|
||||
public string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// The base address for this API client
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
internal protected string BaseAddress { get; }
|
||||
public string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Options
|
||||
/// Output the original string data along with the deserialized object
|
||||
/// </summary>
|
||||
public ApiClientOptions Options { get; }
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
protected static int lastId;
|
||||
protected static int _lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
protected static object idLock = new();
|
||||
protected static object _idLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
@ -110,18 +95,39 @@ namespace CryptoExchange.Net
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
public ApiOptions ApiOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Options
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">Logger</param>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
/// <param name="apiOptions">Api client options</param>
|
||||
protected BaseApiClient(Log log, ClientOptions clientOptions, ApiClientOptions apiOptions)
|
||||
/// <param name="apiOptions">Api options</param>
|
||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||
{
|
||||
Options = apiOptions;
|
||||
_log = log;
|
||||
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? clientOptions.ApiCredentials?.Copy();
|
||||
BaseAddress = apiOptions.BaseAddress;
|
||||
_logger = logger;
|
||||
|
||||
ClientOptions = clientOptions;
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -134,9 +140,11 @@ namespace CryptoExchange.Net
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
_apiCredentials = credentials?.Copy();
|
||||
_created = false;
|
||||
_authenticationProvider = null;
|
||||
if (credentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -149,7 +157,7 @@ namespace CryptoExchange.Net
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
_log.Write(LogLevel.Error, info);
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
@ -188,7 +196,7 @@ namespace CryptoExchange.Net
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
_log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(tokenResult.Error);
|
||||
}
|
||||
|
||||
@ -214,20 +222,20 @@ namespace CryptoExchange.Net
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
_log.Write(LogLevel.Error, info);
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
_log.Write(LogLevel.Error, info);
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
_log.Write(LogLevel.Error, info);
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
@ -250,23 +258,21 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (Options.OutputOriginalData == true || _log.Level == LogLevel.Trace)
|
||||
if (OutputOriginalData == true)
|
||||
{
|
||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
|
||||
var result = Deserialize<T>(data, serializer, requestId);
|
||||
if (Options.OutputOriginalData == true)
|
||||
result.OriginalData = data;
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
_log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
@ -284,7 +290,7 @@ namespace CryptoExchange.Net
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
@ -302,7 +308,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -321,7 +327,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
@ -338,10 +344,10 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected static int NextId()
|
||||
{
|
||||
lock (idLock)
|
||||
lock (_idLock)
|
||||
{
|
||||
lastId += 1;
|
||||
return lastId;
|
||||
_lastId += 1;
|
||||
return _lastId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,8 +357,7 @@ namespace CryptoExchange.Net
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
_apiCredentials?.Dispose();
|
||||
AuthenticationProvider?.Credentials?.Dispose();
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -16,37 +17,48 @@ namespace CryptoExchange.Net
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
internal string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal Log log;
|
||||
protected internal ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public ClientOptions ClientOptions { get; }
|
||||
public ExchangeOptions ClientOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseClient(string name, ClientOptions options)
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string name)
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
log = new Log(name);
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
options.OnLoggingChanged += HandleLogConfigChange;
|
||||
|
||||
ClientOptions = options;
|
||||
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
Name = name;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
/// <summary>
|
||||
/// Initialize the client with the specified options
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public virtual void Initialize(ExchangeOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -65,27 +77,20 @@ namespace CryptoExchange.Net
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||
{
|
||||
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
|
||||
if (ClientOptions == null)
|
||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||
|
||||
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
|
||||
ApiClients.Add(apiClient);
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a change in the client options log config
|
||||
/// </summary>
|
||||
private void HandleLogConfigChange()
|
||||
{
|
||||
log.UpdateWriters(ClientOptions.LogWriters);
|
||||
log.Level = ClientOptions.LogLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Disposing client");
|
||||
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
|
||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@ -17,12 +15,10 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseRestClient(string name, ClientOptions options) : base(name, options)
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -21,7 +19,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool disposing;
|
||||
protected bool _disposing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
@ -34,9 +32,9 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseSocketClient(string name, ClientOptions options) : base(name, options)
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
|
||||
{
|
||||
}
|
||||
|
||||
@ -65,7 +63,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -88,7 +86,7 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
|
@ -8,8 +8,8 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@ -24,6 +24,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
||||
|
||||
@ -38,36 +39,39 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for this client
|
||||
/// </summary>
|
||||
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
|
||||
|
||||
/// <summary>
|
||||
/// List of rate limiters
|
||||
/// </summary>
|
||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Options
|
||||
/// </summary>
|
||||
internal ClientOptions ClientOptions { get; set; }
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">Logger</param>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="httpClient">HttpClient to use</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions) : base(log, options, apiOptions)
|
||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
ClientOptions = options;
|
||||
|
||||
RequestFactory.Configure(apiOptions.RequestTimeout, options.Proxy, apiOptions.HttpClient);
|
||||
RequestFactory.Configure(options.RequestTimeout, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -203,7 +207,7 @@ namespace CryptoExchange.Net
|
||||
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||
if (!syncTimeResult)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
_logger.Log(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
return syncTimeResult.As<IRequest>(default);
|
||||
}
|
||||
}
|
||||
@ -213,7 +217,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
foreach (var limiter in RateLimiters)
|
||||
{
|
||||
var limitResult = await limiter.LimitRequestAsync(_log, uri.AbsolutePath, method, signed, Options.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||
var limitResult = await limiter.LimitRequestAsync(_logger, uri.AbsolutePath, method, signed, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, ApiOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult.Success)
|
||||
return new CallResult<IRequest>(limitResult.Error!);
|
||||
}
|
||||
@ -221,11 +225,11 @@ namespace CryptoExchange.Net
|
||||
|
||||
if (signed && AuthenticationProvider == null)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
_logger.Log(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||
_logger.Log(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||
|
||||
@ -238,7 +242,7 @@ namespace CryptoExchange.Net
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
|
||||
TotalRequestsMade++;
|
||||
_log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||
_logger.Log(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
|
||||
return new CallResult<IRequest>(request);
|
||||
}
|
||||
|
||||
@ -263,6 +267,7 @@ namespace CryptoExchange.Net
|
||||
sw.Stop();
|
||||
var statusCode = response.StatusCode;
|
||||
var headers = response.ResponseHeaders;
|
||||
var responseLength = response.ContentLength;
|
||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@ -272,25 +277,26 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
responseLength ??= data.Length;
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
_log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||
_logger.Log(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(OutputOriginalData ? (": " + data) : "")}");
|
||||
|
||||
if (!expectedEmptyResponse)
|
||||
{
|
||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
// Let the library implementation see if it is an error response, and if so parse the error
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
|
||||
// Not an error, so continue deserializing
|
||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -299,16 +305,16 @@ namespace CryptoExchange.Net
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
// Not empty, and not json
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
// Error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
}
|
||||
|
||||
// Empty success response; okay
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -319,7 +325,7 @@ namespace CryptoExchange.Net
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, 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
|
||||
@ -327,7 +333,7 @@ namespace CryptoExchange.Net
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, Options.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.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -335,36 +341,36 @@ namespace CryptoExchange.Net
|
||||
// Http status code indicates error
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
var parseResult = ValidateJson(data);
|
||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||
if (error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -542,14 +548,14 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
@ -578,7 +584,7 @@ namespace CryptoExchange.Net
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -28,30 +28,37 @@ namespace CryptoExchange.Net
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
/// </summary>
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
|
||||
/// <summary>
|
||||
/// Keep alive interval for websocket connection
|
||||
/// </summary>
|
||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<byte[], string>? dataInterpreterBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<string, string>? dataInterpreterString;
|
||||
|
||||
/// <summary>
|
||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||
/// </summary>
|
||||
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
|
||||
|
||||
/// <summary>
|
||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
|
||||
/// </summary>
|
||||
protected Task? periodicTask;
|
||||
|
||||
/// <summary>
|
||||
/// Wait event for the periodicTask
|
||||
/// </summary>
|
||||
@ -87,6 +94,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => socketConnections.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions
|
||||
{
|
||||
@ -99,24 +107,29 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketApiClientOptions Options => (SocketApiClientOptions)base.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Options
|
||||
/// </summary>
|
||||
internal ClientOptions ClientOptions { get; set; }
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">log</param>
|
||||
/// <param name="logger">log</param>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions) : base(log, options, apiOptions)
|
||||
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
ClientOptions = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -142,7 +155,7 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
return SubscribeAsync(Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -190,11 +203,11 @@ namespace CryptoExchange.Net
|
||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
|
||||
if (subscription == null)
|
||||
{
|
||||
_log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Options.SocketSubscriptionsCombineTarget == 1)
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
@ -218,7 +231,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
|
||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
@ -228,7 +241,7 @@ namespace CryptoExchange.Net
|
||||
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
@ -243,12 +256,12 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
{
|
||||
_log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}, false);
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
|
||||
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
@ -262,7 +275,7 @@ namespace CryptoExchange.Net
|
||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||
{
|
||||
CallResult<object>? callResult = null;
|
||||
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||
|
||||
if (callResult?.Success == true)
|
||||
{
|
||||
@ -285,7 +298,7 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
|
||||
{
|
||||
return QueryAsync<T>(Options.BaseAddress, request, authenticated);
|
||||
return QueryAsync<T>(BaseAddress, request, authenticated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -312,7 +325,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
if (Options.SocketSubscriptionsCombineTarget == 1)
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Can release early when only a single sub per connection
|
||||
semaphoreSlim.Release();
|
||||
@ -331,7 +344,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
|
||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
@ -348,7 +361,7 @@ namespace CryptoExchange.Net
|
||||
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
||||
{
|
||||
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, null, data =>
|
||||
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, data =>
|
||||
{
|
||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||
return false;
|
||||
@ -375,17 +388,17 @@ namespace CryptoExchange.Net
|
||||
if (!connectResult)
|
||||
return new CallResult<bool>(connectResult.Error!);
|
||||
|
||||
if (Options.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(Options.DelayAfterConnect).ConfigureAwait(false);
|
||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||
_logger.Log(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||
if (socket.Connected)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
@ -411,6 +424,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the query</returns>
|
||||
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)] out CallResult<T>? callResult);
|
||||
|
||||
/// <summary>
|
||||
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter).
|
||||
/// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
|
||||
@ -425,6 +439,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the subscription request</returns>
|
||||
protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
|
||||
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
|
||||
@ -434,6 +449,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="request">The subscription request</param>
|
||||
/// <returns>True if the message is for the subscription which sent the request</returns>
|
||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
|
||||
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
|
||||
@ -443,12 +459,14 @@ namespace CryptoExchange.Net
|
||||
/// <param name="identifier">The string identifier of the handler</param>
|
||||
/// <returns>True if the message is for the handler which has the identifier</returns>
|
||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection that should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected internal abstract Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socketConnection);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway
|
||||
/// </summary>
|
||||
@ -485,18 +503,18 @@ namespace CryptoExchange.Net
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
||||
dataHandler(new DataEvent<T>(stringData, null, Options.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
dataHandler(new DataEvent<T>(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
return;
|
||||
}
|
||||
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||
if (!desResult)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, Options.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
}
|
||||
|
||||
var subscription = request == null
|
||||
@ -566,7 +584,7 @@ namespace CryptoExchange.Net
|
||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||
if (result != null)
|
||||
{
|
||||
if (result.SubscriptionCount < Options.SocketSubscriptionsCombineTarget || (socketConnections.Count >= Options.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= Options.SocketSubscriptionsCombineTarget)))
|
||||
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
return new CallResult<SocketConnection>(result);
|
||||
@ -576,16 +594,16 @@ namespace CryptoExchange.Net
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||
_logger.Log(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||
return connectionAddress.As<SocketConnection>(null);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
_log.Write(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||
_logger.Log(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||
|
||||
// Create new socket
|
||||
var socket = CreateSocket(connectionAddress.Data!);
|
||||
var socketConnection = new SocketConnection(_log, this, socket, address);
|
||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
foreach (var kvp in genericHandlers)
|
||||
{
|
||||
@ -627,15 +645,15 @@ namespace CryptoExchange.Net
|
||||
/// <param name="address">The address to connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||
=> new(new Uri(address), Options.AutoReconnect)
|
||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
||||
{
|
||||
DataInterpreterBytes = dataInterpreterBytes,
|
||||
DataInterpreterString = dataInterpreterString,
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
ReconnectInterval = Options.ReconnectInterval,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = Options.SocketNoDataTimeout
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -645,8 +663,8 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual IWebsocket CreateSocket(string address)
|
||||
{
|
||||
var socket = SocketFactory.CreateWebsocket(_log, GetWebSocketParameters(address));
|
||||
_log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
||||
_logger.Log(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||
return socket;
|
||||
}
|
||||
|
||||
@ -682,7 +700,7 @@ namespace CryptoExchange.Net
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
_log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||
|
||||
try
|
||||
{
|
||||
@ -690,7 +708,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -719,7 +737,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null || connection == null)
|
||||
return false;
|
||||
|
||||
_log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
|
||||
_logger.Log(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
|
||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
@ -734,7 +752,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
_log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -744,12 +762,12 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
_log.Write(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 socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
@ -761,7 +779,7 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_log.Write(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
@ -798,7 +816,7 @@ namespace CryptoExchange.Net
|
||||
periodicEvent?.Dispose();
|
||||
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
_ = UnsubscribeAllAsync();
|
||||
}
|
||||
semaphoreSlim?.Dispose();
|
||||
|
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
public class ArrayConverter : JsonConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
@ -100,12 +100,16 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
if (value != null && property.PropertyType.IsInstanceOfType(value))
|
||||
{
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value is JToken token)
|
||||
{
|
||||
if (token.Type == JTokenType.Null)
|
||||
value = null;
|
||||
}
|
||||
|
||||
if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
@ -175,10 +179,10 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||
(T?)attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
|
||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||
(T?)attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// The enum->string mapping
|
||||
/// </summary>
|
||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
||||
private readonly bool quotes;
|
||||
private readonly bool _quotes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="useQuotes"></param>
|
||||
protected BaseConverter(bool useQuotes)
|
||||
{
|
||||
quotes = useQuotes;
|
||||
_quotes = useQuotes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = value == null? null: GetValue((T) value);
|
||||
if (quotes)
|
||||
if (_quotes)
|
||||
writer.WriteValue(stringValue);
|
||||
else
|
||||
writer.WriteRawValue(stringValue);
|
||||
|
@ -12,9 +12,9 @@ namespace CryptoExchange.Net.Converters
|
||||
public class DateTimeConverter: JsonConverter
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
@ -134,7 +134,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * ticksPerSecond));
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
@ -146,13 +146,13 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
/// <param name="microseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * ticksPerMicrosecond));
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="nanoseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * ticksPerNanosecond));
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
@ -173,14 +173,14 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerMicrosecond);
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerNanosecond);
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
@ -6,18 +6,18 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>5.4.3</PackageVersion>
|
||||
<AssemblyVersion>5.4.3</AssemblyVersion>
|
||||
<FileVersion>5.4.3</FileVersion>
|
||||
<PackageVersion>6.0.0</PackageVersion>
|
||||
<AssemblyVersion>6.0.0</AssemblyVersion>
|
||||
<FileVersion>6.0.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>5.4.3 - Fixed potential threading exception in socket connection</PackageReleaseNotes>
|
||||
<PackageReleaseNotes></PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
@ -45,10 +45,11 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -43,7 +43,9 @@ namespace CryptoExchange.Net
|
||||
|
||||
var offset = value % step.Value;
|
||||
if(roundingType == RoundingType.Down)
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
|
@ -6,7 +6,6 @@ using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@ -90,31 +89,6 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="converter"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value, JsonConverter converter)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a query string of the specified parameters
|
||||
/// </summary>
|
||||
@ -129,7 +103,9 @@ namespace CryptoExchange.Net
|
||||
foreach (var arrayEntry in arraysParameters)
|
||||
{
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
}
|
||||
else
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
@ -160,7 +136,9 @@ namespace CryptoExchange.Net
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
}
|
||||
return formData.ToString();
|
||||
}
|
||||
@ -224,7 +202,11 @@ namespace CryptoExchange.Net
|
||||
if (b1 != b2) return false;
|
||||
}
|
||||
}
|
||||
else return false;
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
@ -252,9 +234,9 @@ namespace CryptoExchange.Net
|
||||
/// String to JToken
|
||||
/// </summary>
|
||||
/// <param name="stringData"></param>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static JToken? ToJToken(this string stringData, Log? log = null)
|
||||
public static JToken? ToJToken(this string stringData, ILogger? logger = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stringData))
|
||||
return null;
|
||||
@ -266,15 +248,15 @@ namespace CryptoExchange.Net
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -288,8 +270,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||
{
|
||||
if (!allowedValues.Contains(value))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -302,8 +286,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||
{
|
||||
if (value < minValue || value > maxValue)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -437,7 +423,9 @@ namespace CryptoExchange.Net
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
@ -466,13 +454,14 @@ namespace CryptoExchange.Net
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add parameter to URI
|
||||
/// </summary>
|
||||
|
@ -7,6 +7,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
public interface IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Base address
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this API client
|
||||
/// </summary>
|
||||
|
@ -1,5 +1,5 @@
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="requestWeight">The weight of the request</param>
|
||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||
/// <returns>The time in milliseconds spend waiting</returns>
|
||||
Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Configure the requests created by this factory
|
||||
/// </summary>
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
/// <param name="proxy">Proxy settings to use</param>
|
||||
/// <param name="httpClient">Optional shared http client instance</param>
|
||||
void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? httpClient=null);
|
||||
void Configure(TimeSpan requestTimeout, HttpClient? httpClient=null);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
bool IsSuccessStatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the response in bytes
|
||||
/// </summary>
|
||||
long? ContentLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@ -12,7 +13,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
ClientOptions ClientOptions { get; }
|
||||
ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of requests made with this client
|
||||
|
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@ -23,10 +24,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
SocketApiClientOptions Options { get; }
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@ -14,7 +15,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
ClientOptions ClientOptions { get; }
|
||||
ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Incoming kilobytes per second of data
|
||||
|
@ -1,5 +1,5 @@
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@ -11,9 +11,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="logger">The logger</param>
|
||||
/// <param name="parameters">The parameters to use for the connection</param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
|
||||
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// ILogger implementation for logging to the console
|
||||
/// </summary>
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Console.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Default log writer, uses Trace.WriteLine
|
||||
/// </summary>
|
||||
public class DebugLogger: ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Log implementation
|
||||
/// </summary>
|
||||
public class Log
|
||||
{
|
||||
/// <summary>
|
||||
/// List of ILogger implementations to forward the message to
|
||||
/// </summary>
|
||||
private List<ILogger> writers;
|
||||
|
||||
/// <summary>
|
||||
/// The verbosity of the logging, anything more verbose will not be forwarded to the writers
|
||||
/// </summary>
|
||||
public LogLevel? Level { get; set; } = LogLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="clientName">The name of the client the logging is used in</param>
|
||||
public Log(string clientName)
|
||||
{
|
||||
ClientName = clientName;
|
||||
writers = new List<ILogger>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the writers
|
||||
/// </summary>
|
||||
/// <param name="textWriters"></param>
|
||||
public void UpdateWriters(List<ILogger> textWriters)
|
||||
{
|
||||
lock (_lock)
|
||||
writers = textWriters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a log entry
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The verbosity of the message</param>
|
||||
/// <param name="message">The message to log</param>
|
||||
public void Write(LogLevel logLevel, string message)
|
||||
{
|
||||
if (Level != null && (int)logLevel < (int)Level)
|
||||
return;
|
||||
|
||||
var logMessage = $"{ClientName,-10} | {message}";
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var writer in writers)
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.Log(logLevel, logMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Can't write to the logging so where else to output..
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -12,7 +13,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public class AsyncResetEvent : IDisposable
|
||||
{
|
||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||
private readonly Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
private bool _signaled;
|
||||
private readonly bool _reset;
|
||||
|
||||
@ -49,7 +50,13 @@ namespace CryptoExchange.Net.Objects
|
||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
||||
var registration = cancellationSource.Token.Register(() =>
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
lock (_waits)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
|
||||
// Not the cleanest but it works
|
||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||
}
|
||||
}, useSynchronizationContext: false);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@ -38,6 +39,12 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -128,6 +135,24 @@ namespace CryptoExchange.Net.Objects
|
||||
return new CallResult<K>(data, OriginalData, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDataless()
|
||||
{
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new CallResult(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@ -138,6 +163,12 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -226,6 +257,12 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -259,6 +296,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public HttpStatusCode? ResponseStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes of the response
|
||||
/// </summary>
|
||||
public long? ResponseLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
@ -275,6 +317,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="responseLength"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
@ -286,6 +329,7 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
@ -297,6 +341,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
ResponseLength = responseLength;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
@ -304,11 +349,28 @@ namespace CryptoExchange.Net.Objects
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, default, error) { }
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
@ -318,25 +380,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -347,7 +391,20 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Success ? $"Success response" : $"Error response: {Error}");
|
||||
if (ResponseLength != null)
|
||||
sb.Append($", {ResponseLength} bytes");
|
||||
if (ResponseTime != null)
|
||||
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,329 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Linq;
|
||||
//using System.Net.Http;
|
||||
//using CryptoExchange.Net.Authentication;
|
||||
//using CryptoExchange.Net.Interfaces;
|
||||
//using CryptoExchange.Net.Logging;
|
||||
//using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
public abstract class ClientOptions
|
||||
{
|
||||
internal event Action? OnLoggingChanged;
|
||||
//namespace CryptoExchange.Net.Objects
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// Client options
|
||||
// /// </summary>
|
||||
// public abstract class ClientOptions
|
||||
// {
|
||||
// internal event Action? OnLoggingChanged;
|
||||
|
||||
private LogLevel _logLevel = LogLevel.Information;
|
||||
/// <summary>
|
||||
/// The minimum log level to output
|
||||
/// </summary>
|
||||
public LogLevel LogLevel
|
||||
{
|
||||
get => _logLevel;
|
||||
set
|
||||
{
|
||||
_logLevel = value;
|
||||
OnLoggingChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
// private LogLevel _logLevel = LogLevel.Information;
|
||||
// /// <summary>
|
||||
// /// The minimum log level to output
|
||||
// /// </summary>
|
||||
// public LogLevel LogLevel
|
||||
// {
|
||||
// get => _logLevel;
|
||||
// set
|
||||
// {
|
||||
// _logLevel = value;
|
||||
// OnLoggingChanged?.Invoke();
|
||||
// }
|
||||
// }
|
||||
|
||||
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
||||
/// <summary>
|
||||
/// The log writers
|
||||
/// </summary>
|
||||
public List<ILogger> LogWriters
|
||||
{
|
||||
get => _logWriters;
|
||||
set
|
||||
{
|
||||
_logWriters = value;
|
||||
OnLoggingChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
// private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
||||
// /// <summary>
|
||||
// /// The log writers
|
||||
// /// </summary>
|
||||
// public List<ILogger> LogWriters
|
||||
// {
|
||||
// get => _logWriters;
|
||||
// set
|
||||
// {
|
||||
// _logWriters = value;
|
||||
// OnLoggingChanged?.Invoke();
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Proxy to use when connecting
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
// /// <summary>
|
||||
// /// Proxy to use when connecting
|
||||
// /// </summary>
|
||||
// public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
// /// <summary>
|
||||
// /// The api credentials used for signing requests to this API.
|
||||
// /// </summary>
|
||||
// public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ClientOptions()
|
||||
{
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// public ClientOptions()
|
||||
// {
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="clientOptions">Copy values for the provided options</param>
|
||||
public ClientOptions(ClientOptions? clientOptions)
|
||||
{
|
||||
if (clientOptions == null)
|
||||
return;
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="clientOptions">Copy values for the provided options</param>
|
||||
// public ClientOptions(ClientOptions? clientOptions)
|
||||
// {
|
||||
// if (clientOptions == null)
|
||||
// return;
|
||||
|
||||
LogLevel = clientOptions.LogLevel;
|
||||
LogWriters = clientOptions.LogWriters.ToList();
|
||||
Proxy = clientOptions.Proxy;
|
||||
ApiCredentials = clientOptions.ApiCredentials?.Copy();
|
||||
}
|
||||
// LogLevel = clientOptions.LogLevel;
|
||||
// LogWriters = clientOptions.LogWriters.ToList();
|
||||
// Proxy = clientOptions.Proxy;
|
||||
// ApiCredentials = clientOptions.ApiCredentials?.Copy();
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy values for the provided options</param>
|
||||
/// <param name="newValues">Copy values for the provided options</param>
|
||||
internal ClientOptions(ClientOptions baseOptions, ClientOptions? newValues)
|
||||
{
|
||||
Proxy = newValues?.Proxy ?? baseOptions.Proxy;
|
||||
LogLevel = baseOptions.LogLevel;
|
||||
LogWriters = baseOptions.LogWriters.ToList();
|
||||
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseOptions">Copy values for the provided options</param>
|
||||
// /// <param name="newValues">Copy values for the provided options</param>
|
||||
// internal ClientOptions(ClientOptions baseOptions, ClientOptions? newValues)
|
||||
// {
|
||||
// Proxy = newValues?.Proxy ?? baseOptions.Proxy;
|
||||
// LogLevel = baseOptions.LogLevel;
|
||||
// LogWriters = baseOptions.LogWriters.ToList();
|
||||
// ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||
// }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
|
||||
}
|
||||
}
|
||||
// /// <inheritdoc />
|
||||
// public override string ToString()
|
||||
// {
|
||||
// return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// API client options
|
||||
/// </summary>
|
||||
public class ApiClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
// /// <summary>
|
||||
// /// API client options
|
||||
// /// </summary>
|
||||
// public class ApiClientOptions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
// /// </summary>
|
||||
// public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the API
|
||||
/// </summary>
|
||||
public string BaseAddress { get; set; }
|
||||
// /// <summary>
|
||||
// /// The base address of the API
|
||||
// /// </summary>
|
||||
// public string BaseAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
// /// <summary>
|
||||
// /// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
||||
// /// </summary>
|
||||
// public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
#pragma warning disable 8618 // Will always get filled by the implementation
|
||||
public ApiClientOptions()
|
||||
{
|
||||
}
|
||||
#pragma warning restore 8618
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
//#pragma warning disable 8618 // Will always get filled by the implementation
|
||||
// public ApiClientOptions()
|
||||
// {
|
||||
// }
|
||||
//#pragma warning restore 8618
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Base address for the API</param>
|
||||
public ApiClientOptions(string baseAddress)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseAddress">Base address for the API</param>
|
||||
// public ApiClientOptions(string baseAddress)
|
||||
// {
|
||||
// BaseAddress = baseAddress;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy values for the provided options</param>
|
||||
/// <param name="newValues">Copy values for the provided options</param>
|
||||
public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
|
||||
{
|
||||
BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
|
||||
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||
OutputOriginalData = newValues?.OutputOriginalData ?? baseOptions.OutputOriginalData;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseOptions">Copy values for the provided options</param>
|
||||
// /// <param name="newValues">Copy values for the provided options</param>
|
||||
// public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
|
||||
// {
|
||||
// BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
|
||||
// ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||
// OutputOriginalData = newValues?.OutputOriginalData ?? baseOptions.OutputOriginalData;
|
||||
// }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"OutputOriginalData: {OutputOriginalData}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
|
||||
}
|
||||
}
|
||||
// /// <inheritdoc />
|
||||
// public override string ToString()
|
||||
// {
|
||||
// return $"OutputOriginalData: {OutputOriginalData}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Rest API client options
|
||||
/// </summary>
|
||||
public class RestApiClientOptions: ApiClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The time the server has to respond to a request before timing out
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
// /// <summary>
|
||||
// /// Rest API client options
|
||||
// /// </summary>
|
||||
// public class RestApiClientOptions: ApiClientOptions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// The time the server has to respond to a request before timing out
|
||||
// /// </summary>
|
||||
// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
|
||||
/// </summary>
|
||||
public HttpClient? HttpClient { get; set; }
|
||||
// /// <summary>
|
||||
// /// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
|
||||
// /// </summary>
|
||||
// public HttpClient? HttpClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
// /// <summary>
|
||||
// /// List of rate limiters to use
|
||||
// /// </summary>
|
||||
// public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when a call would exceed the rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
// /// <summary>
|
||||
// /// What to do when a call would exceed the rate limit
|
||||
// /// </summary>
|
||||
// public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
// /// <summary>
|
||||
// /// Whether or not to automatically sync the local time with the server time
|
||||
// /// </summary>
|
||||
// public bool AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
// /// <summary>
|
||||
// /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
// /// </summary>
|
||||
// public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RestApiClientOptions()
|
||||
{
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// public RestApiClientOptions()
|
||||
// {
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Base address for the API</param>
|
||||
public RestApiClientOptions(string baseAddress): base(baseAddress)
|
||||
{
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseAddress">Base address for the API</param>
|
||||
// public RestApiClientOptions(string baseAddress): base(baseAddress)
|
||||
// {
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOn">Copy values for the provided options</param>
|
||||
/// <param name="newValues">Copy values for the provided options</param>
|
||||
public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
|
||||
{
|
||||
HttpClient = newValues?.HttpClient ?? baseOn.HttpClient;
|
||||
RequestTimeout = newValues == default ? baseOn.RequestTimeout : newValues.RequestTimeout;
|
||||
RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
|
||||
AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
|
||||
TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
|
||||
RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseOn">Copy values for the provided options</param>
|
||||
// /// <param name="newValues">Copy values for the provided options</param>
|
||||
// public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
|
||||
// {
|
||||
// HttpClient = newValues?.HttpClient ?? baseOn.HttpClient;
|
||||
// RequestTimeout = newValues == default ? baseOn.RequestTimeout : newValues.RequestTimeout;
|
||||
// RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
|
||||
// AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
|
||||
// TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
|
||||
// RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
|
||||
// }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
|
||||
}
|
||||
}
|
||||
// /// <inheritdoc />
|
||||
// public override string ToString()
|
||||
// {
|
||||
// return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Rest API client options
|
||||
/// </summary>
|
||||
public class SocketApiClientOptions : ApiClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
// /// <summary>
|
||||
// /// Rest API client options
|
||||
// /// </summary>
|
||||
// public class SocketApiClientOptions : ApiClientOptions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Whether or not the socket should automatically reconnect when losing connection
|
||||
// /// </summary>
|
||||
// public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
// /// <summary>
|
||||
// /// Time to wait between reconnect attempts
|
||||
// /// </summary>
|
||||
// public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
// /// <summary>
|
||||
// /// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
// /// </summary>
|
||||
// public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The max time to wait for a response after sending a request on the socket before giving a timeout
|
||||
/// </summary>
|
||||
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
// /// <summary>
|
||||
// /// The max time to wait for a response after sending a request on the socket before giving a timeout
|
||||
// /// </summary>
|
||||
// public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <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,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
// /// <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,
|
||||
// /// for example when the server sends intermittent ping requests
|
||||
// /// </summary>
|
||||
// public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
// /// <summary>
|
||||
// /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
// /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
// /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
// /// </summary>
|
||||
// public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
// /// <summary>
|
||||
// /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
// /// </summary>
|
||||
// public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
/// </summary>
|
||||
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||
// /// <summary>
|
||||
// /// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
// /// </summary>
|
||||
// public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SocketApiClientOptions()
|
||||
{
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// public SocketApiClientOptions()
|
||||
// {
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Base address for the API</param>
|
||||
public SocketApiClientOptions(string baseAddress) : base(baseAddress)
|
||||
{
|
||||
}
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseAddress">Base address for the API</param>
|
||||
// public SocketApiClientOptions(string baseAddress) : base(baseAddress)
|
||||
// {
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy values for the provided options</param>
|
||||
/// <param name="newValues">Copy values for the provided options</param>
|
||||
public SocketApiClientOptions(SocketApiClientOptions baseOptions, SocketApiClientOptions? newValues) : base(baseOptions, newValues)
|
||||
{
|
||||
if (baseOptions == null)
|
||||
return;
|
||||
// /// <summary>
|
||||
// /// ctor
|
||||
// /// </summary>
|
||||
// /// <param name="baseOptions">Copy values for the provided options</param>
|
||||
// /// <param name="newValues">Copy values for the provided options</param>
|
||||
// public SocketApiClientOptions(SocketApiClientOptions baseOptions, SocketApiClientOptions? newValues) : base(baseOptions, newValues)
|
||||
// {
|
||||
// if (baseOptions == null)
|
||||
// return;
|
||||
|
||||
AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
|
||||
ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
|
||||
MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||
SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
|
||||
SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
|
||||
SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
|
||||
MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
|
||||
DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
|
||||
}
|
||||
// AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
|
||||
// ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
|
||||
// MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||
// SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
|
||||
// SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
|
||||
// SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
|
||||
// MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
|
||||
// DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
|
||||
// }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
||||
}
|
||||
}
|
||||
// /// <inheritdoc />
|
||||
// public override string ToString()
|
||||
// {
|
||||
// return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : ClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// Base for order book options
|
||||
// /// </summary>
|
||||
// public class OrderBookOptions : ClientOptions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
// /// </summary>
|
||||
// public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
// }
|
||||
|
||||
}
|
||||
//}
|
||||
|
20
CryptoExchange.Net/Objects/Options/ApiOptions.cs
Normal file
20
CryptoExchange.Net/Objects/Options/ApiOptions.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for API usage
|
||||
/// </summary>
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool? OutputOriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
}
|
||||
}
|
37
CryptoExchange.Net/Objects/Options/ExchangeOptions.cs
Normal file
37
CryptoExchange.Net/Objects/Options/ExchangeOptions.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange options
|
||||
/// </summary>
|
||||
public class ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy settings
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The max time a request is allowed to take
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
30
CryptoExchange.Net/Objects/Options/OrderBookOptions.cs
Normal file
30
CryptoExchange.Net/Objects/Options/OrderBookOptions.cs
Normal file
@ -0,0 +1,30 @@
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : OrderBookOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
67
CryptoExchange.Net/Objects/Options/RestApiOptions.cs
Normal file
67
CryptoExchange.Net/Objects/Options/RestApiOptions.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Http api options
|
||||
/// </summary>
|
||||
public class RestApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when a call would exceed the rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool? AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan? TimestampRecalculationInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public virtual T Copy<T>() where T : RestApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
RateLimiters = RateLimiters,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Http API options
|
||||
/// </summary>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class RestApiOptions<TApiCredentials>: RestApiOptions where TApiCredentials: ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
83
CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs
Normal file
83
CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
public class RestExchangeOptions: ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
54
CryptoExchange.Net/Objects/Options/SocketApiOptions.cs
Normal file
54
CryptoExchange.Net/Objects/Options/SocketApiOptions.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket api options
|
||||
/// </summary>
|
||||
public class SocketApiOptions : ApiOptions
|
||||
{
|
||||
/// <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,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan? SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Socket API options
|
||||
/// </summary>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class SocketApiOptions<TApiCredentials> : SocketApiOptions where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
116
CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs
Normal file
116
CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a websocket exchange client
|
||||
/// </summary>
|
||||
public class SocketExchangeOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <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,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
/// </summary>
|
||||
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoReconnect = AutoReconnect,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -101,7 +100,7 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
||||
public async Task<CallResult<int>> LimitRequestAsync(ILogger logger, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
||||
{
|
||||
int totalWaitTime = 0;
|
||||
|
||||
@ -110,7 +109,7 @@ namespace CryptoExchange.Net.Objects
|
||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||
if(endpointLimit != null)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@ -138,7 +137,7 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(log, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@ -146,7 +145,7 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
else
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@ -166,7 +165,7 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
if (!apiLimit.OnlyForSignedRequests)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@ -186,7 +185,7 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(log, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@ -202,7 +201,7 @@ namespace CryptoExchange.Net.Objects
|
||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||
if (totalLimit != null)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@ -212,7 +211,7 @@ namespace CryptoExchange.Net.Objects
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
|
||||
private static async Task<CallResult<int>> ProcessTopic(Log log, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
||||
private static async Task<CallResult<int>> ProcessTopic(ILogger logger, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
@ -256,11 +255,11 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
historyTopic.Semaphore.Release();
|
||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||
log.Write(LogLevel.Warning, msg);
|
||||
logger.Log(LogLevel.Warning, msg);
|
||||
return new CallResult<int>(new RateLimitError(msg));
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
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);
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
@ -45,7 +44,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
public Log Log { get; }
|
||||
public ILogger Logger { get; }
|
||||
/// <summary>
|
||||
/// Should synchronize time
|
||||
/// </summary>
|
||||
@ -62,13 +61,13 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="recalculationInterval"></param>
|
||||
/// <param name="syncTime"></param>
|
||||
/// <param name="syncState"></param>
|
||||
public TimeSyncInfo(Log log, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
|
||||
public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
|
||||
{
|
||||
Log = log;
|
||||
Logger = logger;
|
||||
SyncTime = syncTime;
|
||||
RecalculationInterval = recalculationInterval;
|
||||
TimeSyncState = syncState;
|
||||
@ -83,12 +82,12 @@ namespace CryptoExchange.Net.Objects
|
||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
||||
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
||||
{
|
||||
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
|
||||
TimeSyncState.TimeOffset = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
TimeSyncState.TimeOffset = offset;
|
||||
}
|
||||
}
|
||||
|
52
CryptoExchange.Net/Objects/TraceLogger.cs
Normal file
52
CryptoExchange.Net/Objects/TraceLogger.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trace logger provider for creating trace loggers
|
||||
/// </summary>
|
||||
public class TraceLoggerProvider : ILoggerProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName);
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trace logger
|
||||
/// </summary>
|
||||
public class TraceLogger : ILogger
|
||||
{
|
||||
private string? _categoryName;
|
||||
private LogLevel _logLevel;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="categoryName"></param>
|
||||
/// <param name="level"></param>
|
||||
public TraceLogger(string? categoryName = null, LogLevel level = LogLevel.Trace)
|
||||
{
|
||||
_categoryName = categoryName;
|
||||
_logLevel = level;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel;
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if ((int)logLevel < (int)_logLevel)
|
||||
return;
|
||||
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
39
CryptoExchange.Net/Objects/TradeEnvironment.cs
Normal file
39
CryptoExchange.Net/Objects/TradeEnvironment.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment names
|
||||
/// </summary>
|
||||
public static class TradeEnvironmentNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Live environment
|
||||
/// </summary>
|
||||
public const string Live = "live";
|
||||
/// <summary>
|
||||
/// Testnet environment
|
||||
/// </summary>
|
||||
public const string Testnet = "testnet";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the echange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
public class TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the environment
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
protected TradeEnvironment(string name)
|
||||
{
|
||||
EnvironmentName = name;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,14 +13,17 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// First sequence number in this update
|
||||
/// </summary>
|
||||
public long FirstUpdateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last sequence number in this update
|
||||
/// </summary>
|
||||
public long LastUpdateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of changed/new asks
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||
|
||||
/// <summary>
|
||||
/// List of changed/new bids
|
||||
/// </summary>
|
||||
|
@ -7,8 +7,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -30,7 +30,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
private readonly AsyncResetEvent _queueEvent;
|
||||
private readonly ConcurrentQueue<object> _processQueue;
|
||||
private readonly bool _validateChecksum;
|
||||
private bool _validateChecksum;
|
||||
|
||||
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
|
||||
{
|
||||
@ -42,46 +42,45 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A buffer to store messages received before the initial book snapshot is processed. These messages
|
||||
/// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower
|
||||
/// than the snapshot sequence number will be discarded
|
||||
/// </summary>
|
||||
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
|
||||
protected readonly List<ProcessBufferRangeSequenceEntry> _processBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// The ask list, should only be accessed using the bookLock
|
||||
/// </summary>
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> _asks;
|
||||
|
||||
/// <summary>
|
||||
/// The bid list, should only be accessed using the bookLock
|
||||
/// </summary>
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> _bids;
|
||||
|
||||
/// <summary>
|
||||
/// The log
|
||||
/// </summary>
|
||||
protected Log log;
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1
|
||||
/// the book will resynchronize as it is deemed out of sync
|
||||
/// </summary>
|
||||
protected bool sequencesAreConsecutive;
|
||||
protected bool _sequencesAreConsecutive;
|
||||
|
||||
/// <summary>
|
||||
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
|
||||
/// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the
|
||||
/// server handle this
|
||||
/// </summary>
|
||||
protected bool strictLevels;
|
||||
protected bool _strictLevels;
|
||||
|
||||
/// <summary>
|
||||
/// If the initial snapshot of the book has been set
|
||||
/// </summary>
|
||||
protected bool bookSet;
|
||||
protected bool _bookSet;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of levels for this book
|
||||
@ -102,7 +101,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
var old = _status;
|
||||
_status = value;
|
||||
log.Write(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
|
||||
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
|
||||
OnStatusChange?.Invoke(old, _status);
|
||||
}
|
||||
}
|
||||
@ -137,7 +136,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return asks.Select(a => a.Value).ToList();
|
||||
return _asks.Select(a => a.Value).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +146,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return bids.Select(a => a.Value).ToList();
|
||||
return _bids.Select(a => a.Value).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,7 +166,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||
return _bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,7 +176,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||
return _asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,32 +191,39 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to use. If not provided will create a TraceLogger</param>
|
||||
/// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</param>
|
||||
/// <param name="symbol">The symbol the order book is for</param>
|
||||
/// <param name="options">The options for the order book</param>
|
||||
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
|
||||
protected SymbolOrderBook(ILogger? logger, string id, string symbol)
|
||||
{
|
||||
if (symbol == null)
|
||||
throw new ArgumentNullException(nameof(symbol));
|
||||
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
Id = id;
|
||||
processBuffer = new List<ProcessBufferRangeSequenceEntry>();
|
||||
_processBuffer = new List<ProcessBufferRangeSequenceEntry>();
|
||||
_processQueue = new ConcurrentQueue<object>();
|
||||
_queueEvent = new AsyncResetEvent(false, true);
|
||||
|
||||
_validateChecksum = options.ChecksumValidationEnabled;
|
||||
Symbol = symbol;
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
|
||||
asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
||||
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
||||
_asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
||||
_bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
||||
|
||||
log = new Log(id) { Level = options.LogLevel };
|
||||
var writers = options.LogWriters ?? new List<ILogger> { new DebugLogger() };
|
||||
log.UpdateWriters(writers.ToList());
|
||||
_logger = logger ?? new TraceLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the order book using the provided options
|
||||
/// </summary>
|
||||
/// <param name="options">The options</param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
protected void Initialize(OrderBookOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
_validateChecksum = options.ChecksumValidationEnabled;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -226,7 +232,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
if (Status != OrderBookStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
|
||||
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} starting");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} starting");
|
||||
_cts = new CancellationTokenSource();
|
||||
ct?.Register(async () =>
|
||||
{
|
||||
@ -236,8 +242,8 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
// Clear any previous messages
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
processBuffer.Clear();
|
||||
bookSet = false;
|
||||
_processBuffer.Clear();
|
||||
_bookSet = false;
|
||||
|
||||
Status = OrderBookStatus.Connecting;
|
||||
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
|
||||
@ -251,7 +257,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (_cts.IsCancellationRequested)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
|
||||
await startResult.Data.CloseAsync().ConfigureAwait(false);
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
@ -267,7 +273,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
|
||||
private void HandleConnectionLost() {
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||
if (Status != OrderBookStatus.Disposed) {
|
||||
Status = OrderBookStatus.Reconnecting;
|
||||
Reset();
|
||||
@ -275,7 +281,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
|
||||
private void HandleConnectionClosed() {
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
}
|
||||
@ -287,7 +293,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <inheritdoc/>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
_cts?.Cancel();
|
||||
_queueEvent.Set();
|
||||
@ -300,7 +306,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||
}
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -314,7 +320,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var amountLeft = baseQuantity;
|
||||
lock (_bookLock)
|
||||
{
|
||||
var list = type == OrderBookEntryType.Ask ? asks : bids;
|
||||
var list = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
|
||||
var step = 0;
|
||||
while (amountLeft > 0)
|
||||
@ -344,7 +350,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var totalBaseQuantity = 0m;
|
||||
lock (_bookLock)
|
||||
{
|
||||
var list = type == OrderBookEntryType.Ask ? asks : bids;
|
||||
var list = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
|
||||
var step = 0;
|
||||
while (quoteQuantityLeft > 0)
|
||||
@ -456,14 +462,14 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// </summary>
|
||||
protected void CheckProcessBuffer()
|
||||
{
|
||||
var pbList = processBuffer.ToList();
|
||||
var pbList = _processBuffer.ToList();
|
||||
if (pbList.Count > 0)
|
||||
log.Write(LogLevel.Debug, $"Processing {pbList.Count} buffered updates");
|
||||
_logger.Log(LogLevel.Debug, $"Processing {pbList.Count} buffered updates");
|
||||
|
||||
foreach (var bufferEntry in pbList)
|
||||
{
|
||||
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
|
||||
processBuffer.Remove(bufferEntry);
|
||||
_processBuffer.Remove(bufferEntry);
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,21 +483,21 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (sequence <= LastSequenceNumber)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
|
||||
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
|
||||
{
|
||||
// Out of sync
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
|
||||
_stopProcessing = true;
|
||||
Resubscribe();
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTime = DateTime.UtcNow;
|
||||
var listToChange = type == OrderBookEntryType.Ask ? asks : bids;
|
||||
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
if (entry.Quantity == 0)
|
||||
{
|
||||
if (!listToChange.ContainsKey(entry.Price))
|
||||
@ -527,7 +533,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
|
||||
{
|
||||
var startWait = DateTime.UtcNow;
|
||||
while (!bookSet && Status == OrderBookStatus.Syncing)
|
||||
while (!_bookSet && Status == OrderBookStatus.Syncing)
|
||||
{
|
||||
if(ct.IsCancellationRequested)
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
@ -569,9 +575,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
// Clear queue
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
|
||||
processBuffer.Clear();
|
||||
asks.Clear();
|
||||
bids.Clear();
|
||||
_processBuffer.Clear();
|
||||
_asks.Clear();
|
||||
_bids.Clear();
|
||||
AskCount = 0;
|
||||
BidCount = 0;
|
||||
|
||||
@ -620,8 +626,8 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_queueEvent.Set();
|
||||
// Clear queue
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
processBuffer.Clear();
|
||||
bookSet = false;
|
||||
_processBuffer.Clear();
|
||||
_bookSet = false;
|
||||
DoReset();
|
||||
}
|
||||
|
||||
@ -638,7 +644,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
success = resyncResult;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
|
||||
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
|
||||
Status = OrderBookStatus.Synced;
|
||||
}
|
||||
|
||||
@ -655,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (_stopProcessing)
|
||||
{
|
||||
log.Write(LogLevel.Trace, "Skipping message because of resubscribing");
|
||||
_logger.Log(LogLevel.Trace, "Skipping message because of resubscribing");
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -673,21 +679,21 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
lock (_bookLock)
|
||||
{
|
||||
bookSet = true;
|
||||
asks.Clear();
|
||||
_bookSet = true;
|
||||
_asks.Clear();
|
||||
foreach (var ask in item.Asks)
|
||||
asks.Add(ask.Price, ask);
|
||||
bids.Clear();
|
||||
_asks.Add(ask.Price, ask);
|
||||
_bids.Clear();
|
||||
foreach (var bid in item.Bids)
|
||||
bids.Add(bid.Price, bid);
|
||||
_bids.Add(bid.Price, bid);
|
||||
|
||||
LastSequenceNumber = item.EndUpdateId;
|
||||
|
||||
AskCount = asks.Count;
|
||||
BidCount = bids.Count;
|
||||
AskCount = _asks.Count;
|
||||
BidCount = _bids.Count;
|
||||
|
||||
UpdateTime = DateTime.UtcNow;
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
|
||||
CheckProcessBuffer();
|
||||
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
|
||||
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
||||
@ -698,16 +704,16 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
lock (_bookLock)
|
||||
{
|
||||
if (!bookSet)
|
||||
if (!_bookSet)
|
||||
{
|
||||
processBuffer.Add(new ProcessBufferRangeSequenceEntry()
|
||||
_processBuffer.Add(new ProcessBufferRangeSequenceEntry()
|
||||
{
|
||||
Asks = item.Asks,
|
||||
Bids = item.Bids,
|
||||
FirstUpdateId = item.StartUpdateId,
|
||||
LastUpdateId = item.EndUpdateId,
|
||||
});
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -715,12 +721,12 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var (prevBestBid, prevBestAsk) = BestOffers;
|
||||
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
|
||||
|
||||
if (!asks.Any() || !bids.Any())
|
||||
if (!_asks.Any() || !_bids.Any())
|
||||
return;
|
||||
|
||||
if (asks.First().Key < bids.First().Key)
|
||||
if (_asks.First().Key < _bids.First().Key)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {asks.First().Key}, first bid: {bids.First().Key}. Resyncing");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {_asks.First().Key}, first bid: {_bids.First().Key}. Resyncing");
|
||||
_stopProcessing = true;
|
||||
Resubscribe();
|
||||
return;
|
||||
@ -754,7 +760,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (!checksumResult)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
|
||||
_stopProcessing = true;
|
||||
Resubscribe();
|
||||
}
|
||||
@ -778,7 +784,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
|
||||
{
|
||||
// Resubscribing failed, reconnect the socket
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
|
||||
Status = OrderBookStatus.Reconnecting;
|
||||
_ = _subscription!.ReconnectAsync();
|
||||
}
|
||||
@ -793,7 +799,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (lastUpdateId <= LastSequenceNumber)
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -803,23 +809,23 @@ namespace CryptoExchange.Net.OrderBook
|
||||
foreach (var entry in asks)
|
||||
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry);
|
||||
|
||||
if (Levels.HasValue && strictLevels)
|
||||
if (Levels.HasValue && _strictLevels)
|
||||
{
|
||||
while (this.bids.Count > Levels.Value)
|
||||
while (this._bids.Count > Levels.Value)
|
||||
{
|
||||
BidCount--;
|
||||
this.bids.Remove(this.bids.Last().Key);
|
||||
this._bids.Remove(this._bids.Last().Key);
|
||||
}
|
||||
|
||||
while (this.asks.Count > Levels.Value)
|
||||
while (this._asks.Count > Levels.Value)
|
||||
{
|
||||
AskCount--;
|
||||
this.asks.Remove(this.asks.Last().Key);
|
||||
this._asks.Remove(this._asks.Last().Key);
|
||||
}
|
||||
}
|
||||
|
||||
LastSequenceNumber = lastUpdateId;
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,8 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
public class Request : IRequest
|
||||
{
|
||||
private readonly HttpRequestMessage request;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly HttpRequestMessage _request;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Create request object for web request
|
||||
@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <param name="requestId"></param>
|
||||
public Request(HttpRequestMessage request, HttpClient client, int requestId)
|
||||
{
|
||||
httpClient = client;
|
||||
this.request = request;
|
||||
_httpClient = client;
|
||||
_request = request;
|
||||
RequestId = requestId;
|
||||
}
|
||||
|
||||
@ -37,18 +37,18 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <inheritdoc />
|
||||
public string Accept
|
||||
{
|
||||
set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
|
||||
set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpMethod Method
|
||||
{
|
||||
get => request.Method;
|
||||
set => request.Method = value;
|
||||
get => _request.Method;
|
||||
set => _request.Method = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => request.RequestUri;
|
||||
public Uri Uri => _request.RequestUri;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int RequestId { get; }
|
||||
@ -57,31 +57,31 @@ namespace CryptoExchange.Net.Requests
|
||||
public void SetContent(string data, string contentType)
|
||||
{
|
||||
Content = data;
|
||||
request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
_request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddHeader(string key, string value)
|
||||
{
|
||||
request.Headers.Add(key, value);
|
||||
_request.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, IEnumerable<string>> GetHeaders()
|
||||
{
|
||||
return request.Headers.ToDictionary(h => h.Key, h => h.Value);
|
||||
return _request.Headers.ToDictionary(h => h.Key, h => h.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContent(byte[] data)
|
||||
{
|
||||
request.Content = new ByteArrayContent(data);
|
||||
_request.Content = new ByteArrayContent(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
||||
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,37 +11,24 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
private HttpClient? httpClient;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? client = null)
|
||||
public void Configure(TimeSpan requestTimeout, HttpClient? client = null)
|
||||
{
|
||||
if (client == null)
|
||||
_httpClient = client ?? new HttpClient()
|
||||
{
|
||||
HttpMessageHandler handler = new HttpClientHandler()
|
||||
{
|
||||
Proxy = proxy == null ? null : new WebProxy
|
||||
{
|
||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||
}
|
||||
};
|
||||
|
||||
httpClient = new HttpClient(handler) { Timeout = requestTimeout };
|
||||
}
|
||||
else
|
||||
{
|
||||
httpClient = client;
|
||||
}
|
||||
Timeout = requestTimeout
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||
{
|
||||
if (httpClient == null)
|
||||
if (_httpClient == null)
|
||||
throw new InvalidOperationException("Cant create request before configuring http client");
|
||||
|
||||
return new Request(new HttpRequestMessage(method, uri), httpClient, requestId);
|
||||
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,16 +12,19 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
internal class Response : IResponse
|
||||
{
|
||||
private readonly HttpResponseMessage response;
|
||||
private readonly HttpResponseMessage _response;
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpStatusCode StatusCode => response.StatusCode;
|
||||
public HttpStatusCode StatusCode => _response.StatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;
|
||||
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => response.Headers;
|
||||
public long? ContentLength => _response.Content.Headers.ContentLength;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => _response.Headers;
|
||||
|
||||
/// <summary>
|
||||
/// Create response for a http response message
|
||||
@ -29,19 +32,19 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <param name="response">The actual response</param>
|
||||
public Response(HttpResponseMessage response)
|
||||
{
|
||||
this.response = response;
|
||||
this._response = response;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Stream> GetResponseStreamAsync()
|
||||
{
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await _response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Close()
|
||||
{
|
||||
response.Dispose();
|
||||
_response.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
@ -9,7 +8,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -28,8 +26,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
Reconnecting
|
||||
}
|
||||
|
||||
internal static int lastStreamId;
|
||||
private static readonly object streamIdLock = new();
|
||||
internal static int _lastStreamId;
|
||||
private static readonly object _streamIdLock = new();
|
||||
|
||||
private readonly AsyncResetEvent _sendEvent;
|
||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
||||
@ -60,7 +58,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Log
|
||||
/// </summary>
|
||||
protected Log _log;
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
@ -101,14 +99,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnClose;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<string>? OnMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<Exception>? OnError;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnOpen;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnReconnecting;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnReconnected;
|
||||
/// <inheritdoc />
|
||||
@ -117,12 +120,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">The log object to use</param>
|
||||
/// <param name="logger">The log object to use</param>
|
||||
/// <param name="websocketParameters">The parameters for this socket</param>
|
||||
public CryptoExchangeWebSocketClient(Log log, WebSocketParameters websocketParameters)
|
||||
public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters)
|
||||
{
|
||||
Id = NextStreamId();
|
||||
_log = log;
|
||||
_logger = logger;
|
||||
|
||||
Parameters = websocketParameters;
|
||||
_outgoingMessages = new List<DateTime>();
|
||||
@ -178,7 +181,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
private async Task<bool> ConnectInternalAsync()
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} connecting");
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
||||
@ -186,11 +189,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
||||
return false;
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -199,13 +202,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
while (!_stopRequested)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} starting processing tasks");
|
||||
_processState = ProcessState.Processing;
|
||||
var sendTask = SendLoopAsync();
|
||||
var receiveTask = ReceiveLoopAsync();
|
||||
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} processing tasks finished");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} processing tasks finished");
|
||||
|
||||
_processState = ProcessState.WaitingForClose;
|
||||
while (_closeTask == null)
|
||||
@ -233,14 +236,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
while (!_stopRequested)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
|
||||
var task = GetReconnectionUrl?.Invoke();
|
||||
if (task != null)
|
||||
{
|
||||
var reconnectUri = await task.ConfigureAwait(false);
|
||||
if (reconnectUri != null && Parameters.Uri != reconnectUri)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
|
||||
Parameters.Uri = reconnectUri;
|
||||
}
|
||||
}
|
||||
@ -273,7 +276,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
return;
|
||||
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
||||
_sendBuffer.Enqueue(bytes);
|
||||
_sendEvent.Set();
|
||||
}
|
||||
@ -284,7 +287,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_processState != ProcessState.Processing && IsOpen)
|
||||
return;
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} reconnect requested");
|
||||
_closeTask = CloseInternalAsync();
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
}
|
||||
@ -299,18 +302,18 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
if (_closeTask?.IsCompleted == false)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsOpen)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
|
||||
return;
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} closing");
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
finally
|
||||
@ -322,7 +325,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if(_processTask != null)
|
||||
await _processTask.ConfigureAwait(false);
|
||||
OnClose?.Invoke();
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} closed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -374,11 +377,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} disposing");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} disposing");
|
||||
_disposed = true;
|
||||
_socket.Dispose();
|
||||
_ctsSource.Dispose();
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} disposed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -412,14 +415,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
if (start != null)
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
_outgoingMessages.Add(DateTime.UtcNow);
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@ -442,13 +445,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
|
||||
// Make sure we at least let the owner know there was an error
|
||||
_log.Write(LogLevel.Warning, $"Socket {Id} Send loop stopped with exception");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {Id} Send loop stopped with exception");
|
||||
OnError?.Invoke(e);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} Send loop finished");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} Send loop finished");
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,7 +499,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
break;
|
||||
@ -507,7 +510,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// We received data, but it is not complete, write it to a memory stream for reassembling
|
||||
multiPartMessage = true;
|
||||
memoryStream ??= new MemoryStream();
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@ -515,13 +518,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!multiPartMessage)
|
||||
{
|
||||
// Received a complete message and it's not multi part
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
||||
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Received the end of a multipart message, write to memory stream for reassembling
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
@ -549,12 +552,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (receiveResult?.EndOfMessage == true)
|
||||
{
|
||||
// Reassemble complete message from memory stream
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
||||
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
|
||||
memoryStream.Dispose();
|
||||
}
|
||||
else
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -563,13 +566,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
|
||||
// Make sure we at least let the owner know there was an error
|
||||
_log.Write(LogLevel.Warning, $"Socket {Id} Receive loop stopped with exception");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {Id} Receive loop stopped with exception");
|
||||
OnError?.Invoke(e);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} Receive loop finished");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} Receive loop finished");
|
||||
}
|
||||
}
|
||||
|
||||
@ -596,7 +599,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -611,7 +614,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -623,7 +626,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -669,7 +672,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
protected async Task CheckTimeoutAsync()
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
@ -680,7 +683,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
|
||||
_ = ReconnectAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
@ -711,10 +714,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
private static int NextStreamId()
|
||||
{
|
||||
lock (streamIdLock)
|
||||
lock (_streamIdLock)
|
||||
{
|
||||
lastStreamId++;
|
||||
return lastStreamId;
|
||||
_lastStreamId++;
|
||||
return _lastStreamId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -734,8 +737,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
|
||||
{
|
||||
foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list
|
||||
{
|
||||
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
|
||||
_receivedMessages.Remove(msg);
|
||||
}
|
||||
|
||||
_lastReceivedMessagesUpdate = checkTime;
|
||||
}
|
||||
|
@ -12,14 +12,17 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// The timestamp the data was received
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The topic of the update, what symbol/asset etc..
|
||||
/// </summary>
|
||||
public string? Topic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The received data deserialized into an object
|
||||
/// </summary>
|
||||
|
@ -12,14 +12,17 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// The connection the message was received on
|
||||
/// </summary>
|
||||
public SocketConnection Connection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The json object of the data
|
||||
/// </summary>
|
||||
public JToken JsonData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The originally received string data
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp of when the data was received
|
||||
/// </summary>
|
||||
|
@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
public TimeSpan Timeout { get; }
|
||||
public SocketSubscription? Subscription { get; }
|
||||
|
||||
private CancellationTokenSource cts;
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
|
||||
{
|
||||
@ -25,8 +25,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
RequestTimestamp = DateTime.UtcNow;
|
||||
Subscription = subscription;
|
||||
|
||||
cts = new CancellationTokenSource(timeout);
|
||||
cts.Token.Register(Fail, false);
|
||||
_cts = new CancellationTokenSource(timeout);
|
||||
_cts.Token.Register(Fail, false);
|
||||
}
|
||||
|
||||
public bool CheckData(JToken data)
|
||||
|
@ -4,7 +4,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -120,7 +119,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_pausedActivity != value)
|
||||
{
|
||||
_pausedActivity = value;
|
||||
_log.Write(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value);
|
||||
_logger.Log(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value);
|
||||
if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke());
|
||||
else _ = Task.Run(() => ActivityUnpaused?.Invoke());
|
||||
}
|
||||
@ -140,7 +139,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
var oldStatus = _status;
|
||||
_status = value;
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} status changed from {oldStatus} to {_status}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} status changed from {oldStatus} to {_status}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +147,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
private readonly List<SocketSubscription> _subscriptions;
|
||||
private readonly object _subscriptionLock = new();
|
||||
|
||||
private readonly Log _log;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly List<PendingRequest> _pendingRequests;
|
||||
|
||||
@ -162,13 +161,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// New socket connection
|
||||
/// </summary>
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="logger">The logger</param>
|
||||
/// <param name="apiClient">The api client</param>
|
||||
/// <param name="socket">The socket</param>
|
||||
/// <param name="tag"></param>
|
||||
public SocketConnection(Log log, SocketApiClient apiClient, IWebsocket socket, string tag)
|
||||
public SocketConnection(ILogger logger, SocketApiClient apiClient, IWebsocket socket, string tag)
|
||||
{
|
||||
this._log = log;
|
||||
_logger = logger;
|
||||
ApiClient = apiClient;
|
||||
Tag = tag;
|
||||
|
||||
@ -253,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||
if (!reconnectSuccessful)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
|
||||
_logger.Log(LogLevel.Warning, $"Failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
|
||||
await _socket.ReconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@ -274,9 +273,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
protected virtual void HandleError(Exception e)
|
||||
{
|
||||
if (e is WebSocketException wse)
|
||||
_log.Write(LogLevel.Warning, $"Socket {SocketId} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
|
||||
else
|
||||
_log.Write(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -286,14 +285,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
protected virtual void HandleMessage(string data)
|
||||
{
|
||||
var timestamp = DateTime.UtcNow;
|
||||
_log.Write(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
|
||||
if (string.IsNullOrEmpty(data)) return;
|
||||
|
||||
var tokenData = data.ToJToken(_log);
|
||||
var tokenData = data.ToJToken(_logger);
|
||||
if (tokenData == null)
|
||||
{
|
||||
data = $"\"{data}\"";
|
||||
tokenData = data.ToJToken(_log);
|
||||
tokenData = data.ToJToken(_logger);
|
||||
if (tokenData == null)
|
||||
return;
|
||||
}
|
||||
@ -324,7 +323,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Answer to a timed out request, unsub if it is a subscription request
|
||||
if (pendingRequest.Subscription != null)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout");
|
||||
_logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout");
|
||||
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@ -342,23 +341,23 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
// Message was not a request response, check data handlers
|
||||
var messageEvent = new MessageEvent(this, tokenData, ApiClient.Options.OutputOriginalData ? data : null, timestamp);
|
||||
var messageEvent = new MessageEvent(this, tokenData, ApiClient.OutputOriginalData ? data : null, timestamp);
|
||||
var (handled, userProcessTime, subscription) = HandleData(messageEvent);
|
||||
if (!handled && !handledResponse)
|
||||
{
|
||||
if (!ApiClient.UnhandledMessageExpected)
|
||||
_log.Write(LogLevel.Warning, $"Socket {SocketId} Message not handled: " + tokenData);
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Message not handled: " + tokenData);
|
||||
UnhandledMessage?.Invoke(tokenData);
|
||||
}
|
||||
|
||||
var total = DateTime.UtcNow - timestamp;
|
||||
if (userProcessTime.TotalMilliseconds > 500)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processing slow ({(int)total.TotalMilliseconds}ms, {(int)userProcessTime.TotalMilliseconds}ms user code), consider offloading data handling to another thread. " +
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processing slow ({(int)total.TotalMilliseconds}ms, {(int)userProcessTime.TotalMilliseconds}ms user code), consider offloading data handling to another thread. " +
|
||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
||||
}
|
||||
|
||||
_log.Write(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>
|
||||
@ -422,7 +421,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||
return;
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} closing subscription {subscription.Id}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} closing subscription {subscription.Id}");
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
|
||||
@ -434,7 +433,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
if (Status == SocketStatus.Closing)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} already closing");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} already closing");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -445,7 +444,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (shouldCloseConnection)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} closing as there are no more subscriptions");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} closing as there are no more subscriptions");
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -475,7 +474,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
_subscriptions.Add(subscription);
|
||||
if(subscription.UserSubscription)
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} adding new subscription with id {subscription.Id}, total subscriptions on connection: {_subscriptions.Count(s => s.UserSubscription)}");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} adding new subscription with id {subscription.Id}, total subscriptions on connection: {_subscriptions.Count(s => s.UserSubscription)}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -551,7 +550,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
|
||||
_logger.Log(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
|
||||
currentSubscription?.InvokeExceptionHandler(ex);
|
||||
return (false, TimeSpan.Zero, null);
|
||||
}
|
||||
@ -600,7 +599,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="data">The data to send</param>
|
||||
public virtual bool Send(string data)
|
||||
{
|
||||
_log.Write(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
|
||||
try
|
||||
{
|
||||
_socket.Send(data);
|
||||
@ -624,7 +623,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!anySubscriptions)
|
||||
{
|
||||
// No need to resubscribe anything
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} Nothing to resubscribe, closing connection");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} Nothing to resubscribe, closing connection");
|
||||
_ = _socket.CloseAsync();
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
@ -639,12 +638,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
|
||||
if (!authResult)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {SocketId} authentication failed on reconnected socket. Disconnecting and reconnecting.");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} authentication failed on reconnected socket. Disconnecting and reconnecting.");
|
||||
return authResult;
|
||||
}
|
||||
|
||||
Authenticated = true;
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
|
||||
}
|
||||
|
||||
// Get a list of all subscriptions on the socket
|
||||
@ -665,19 +664,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription.Request!).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, "Failed request revitalization: " + result.Error);
|
||||
_logger.Log(LogLevel.Warning, "Failed request revitalization: " + result.Error);
|
||||
return result.As<bool>(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
||||
for (var i = 0; i < subscriptionList.Count; i += ApiClient.Options.MaxConcurrentResubscriptionsPerSocket)
|
||||
for (var i = 0; i < subscriptionList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
||||
{
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||
|
||||
var taskList = new List<Task<CallResult<bool>>>();
|
||||
foreach (var subscription in subscriptionList.Skip(i).Take(ApiClient.Options.MaxConcurrentResubscriptionsPerSocket))
|
||||
foreach (var subscription in subscriptionList.Skip(i).Take(ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
||||
taskList.Add(ApiClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription));
|
||||
|
||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||
@ -691,7 +690,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {SocketId} all subscription successfully resubscribed on reconnected socket.");
|
||||
_logger.Log(LogLevel.Debug, $"Socket {SocketId} all subscription successfully resubscribed on reconnected socket.");
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
|
@ -9,16 +9,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public class UpdateSubscription
|
||||
{
|
||||
private readonly SocketConnection connection;
|
||||
private readonly SocketSubscription subscription;
|
||||
private readonly SocketConnection _connection;
|
||||
private readonly SocketSubscription _subscription;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
||||
/// </summary>
|
||||
public event Action ConnectionLost
|
||||
{
|
||||
add => connection.ConnectionLost += value;
|
||||
remove => connection.ConnectionLost -= value;
|
||||
add => _connection.ConnectionLost += value;
|
||||
remove => _connection.ConnectionLost -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
add => connection.ConnectionClosed += value;
|
||||
remove => connection.ConnectionClosed -= value;
|
||||
add => _connection.ConnectionClosed += value;
|
||||
remove => _connection.ConnectionClosed -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -37,8 +37,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored
|
||||
{
|
||||
add => connection.ConnectionRestored += value;
|
||||
remove => connection.ConnectionRestored -= value;
|
||||
add => _connection.ConnectionRestored += value;
|
||||
remove => _connection.ConnectionRestored -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -46,8 +46,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityPaused
|
||||
{
|
||||
add => connection.ActivityPaused += value;
|
||||
remove => connection.ActivityPaused -= value;
|
||||
add => _connection.ActivityPaused += value;
|
||||
remove => _connection.ActivityPaused -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,8 +55,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityUnpaused
|
||||
{
|
||||
add => connection.ActivityUnpaused += value;
|
||||
remove => connection.ActivityUnpaused -= value;
|
||||
add => _connection.ActivityUnpaused += value;
|
||||
remove => _connection.ActivityUnpaused -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -64,19 +64,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action<Exception> Exception
|
||||
{
|
||||
add => subscription.Exception += value;
|
||||
remove => subscription.Exception -= value;
|
||||
add => _subscription.Exception += value;
|
||||
remove => _subscription.Exception -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The id of the socket
|
||||
/// </summary>
|
||||
public int SocketId => connection.SocketId;
|
||||
public int SocketId => _connection.SocketId;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the subscription
|
||||
/// </summary>
|
||||
public int Id => subscription.Id;
|
||||
public int Id => _subscription.Id;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -85,8 +85,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="subscription">The subscription</param>
|
||||
public UpdateSubscription(SocketConnection connection, SocketSubscription subscription)
|
||||
{
|
||||
this.connection = connection;
|
||||
this.subscription = subscription;
|
||||
this._connection = connection;
|
||||
this._subscription = subscription;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -95,7 +95,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
return connection.CloseAsync(subscription);
|
||||
return _connection.CloseAsync(_subscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -104,7 +104,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public Task ReconnectAsync()
|
||||
{
|
||||
return connection.TriggerReconnectAsync();
|
||||
return _connection.TriggerReconnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -113,7 +113,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
internal async Task UnsubscribeAsync()
|
||||
{
|
||||
await connection.UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -122,7 +122,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
internal async Task<CallResult<bool>> ResubscribeAsync()
|
||||
{
|
||||
return await connection.ResubscribeAsync(subscription).ConfigureAwait(false);
|
||||
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,42 +15,52 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// The uri to connect to
|
||||
/// </summary>
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Headers to send in the connection handshake
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Cookies to send in the connection handshake
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for the connection
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the socket should automatically reconnect when connection is lost
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||
/// </summary>
|
||||
public TimeSpan? Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Interval at which to send ping frames
|
||||
/// </summary>
|
||||
public TimeSpan? KeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of messages to send per second
|
||||
/// </summary>
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Origin header value to send in the connection handshake
|
||||
/// </summary>
|
||||
public string? Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
|
@ -1,5 +1,5 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
@ -9,9 +9,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
public class WebsocketFactory : IWebsocketFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters)
|
||||
public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters)
|
||||
{
|
||||
return new CryptoExchangeWebSocketClient(log, parameters);
|
||||
return new CryptoExchangeWebSocketClient(logger, parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ nav_order: 2
|
||||
|
||||
## How to use the library
|
||||
|
||||
Each implementation generally provides two different clients, which will be the access point for the API's. First of the rest client, which is typically available via [ExchangeName]Client, and a socket client, which is generally named [ExchangeName]SocketClient. For example `BinanceClient` and `BinanceSocketClient`.
|
||||
Each implementation generally provides two different clients, which will be the access point for the API's. First of is the rest client, which is typically available via [ExchangeName]RestClient, and a socket client, which is generally named [ExchangeName]SocketClient. For example `BinanceRestClient` and `BinanceSocketClient`.
|
||||
|
||||
## Rest client
|
||||
The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this:
|
||||
|
||||
- KucoinClient
|
||||
- [ExchangeName]RestClient
|
||||
- SpotApi
|
||||
- Account
|
||||
- ExchangeData
|
||||
@ -21,6 +21,7 @@ The rest client gives access to the Rest endpoint of the API. Rest endpoints are
|
||||
- Trading
|
||||
|
||||
This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints.
|
||||
|
||||
*Requesting ticker info on the spot API*
|
||||
```csharp
|
||||
var client = new KucoinClient();
|
||||
@ -30,7 +31,7 @@ var tickersResult = kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
|
||||
Structuring the client like this should make it easier to find endpoints and allows for separate options and functionality for different API clients. For example, some API's have totally separate API's for futures, with different base addresses and different API credentials, while other API's have implemented this in the same API. Either way, this structure can facilitate a similar interface.
|
||||
|
||||
### Rest API client
|
||||
The Api clients are parts of the total API with a common identifier. In the previous Kucoin example, it separates the Spot and the Futures API. This again is then separated into topics. Most Rest clients implement the following structure:
|
||||
The Api clients are parts of the total API with a common identifier. In the previous example, it separates the Spot and the Futures API. This again is then separated into topics. Most Rest clients implement the following structure:
|
||||
|
||||
**Account**
|
||||
Endpoints related to the user account. This can for example be endpoints for accessing account settings, or getting account balances. The endpoints in this topic will require API credentials to be provided in the client options.
|
||||
@ -44,13 +45,19 @@ Endpoints related to trading. These are endpoints for placing and retrieving ord
|
||||
|
||||
### Processing request responses
|
||||
Each request will return a WebCallResult<T> with the following properties:
|
||||
`ResponseHeaders`: The headers returned from the server
|
||||
`ResponseStatusCode`: The status code as returned by the server
|
||||
`Success`: Whether or not the call was successful. If successful the `Data` property will contain the resulting data, if not successful the `Error` property will contain more details about what the issue was
|
||||
`Error`: Details on what went wrong with a call. Only filled when `Success` == `false`
|
||||
`Data`: Data returned by the server
|
||||
`RequestHeaders`: The headers send to the server in the request message
|
||||
`RequestMethod`: The Http method of the request
|
||||
`RequestUrl`: The url the request was send to
|
||||
`ResponseLength`: The length in bytes of the response message
|
||||
`ResponseTime`: The duration between sending the request and receiving the response
|
||||
`ResponseHeaders`: The headers returned from the server
|
||||
`ResponseStatusCode`: The status code as returned by the server
|
||||
`Success`: Whether or not the call was successful. If successful the `Data` property will contain the resulting data, if not successful the `Error` property will contain more details about what the issue was
|
||||
`Error`: Details on what went wrong with a call. Only filled when `Success` == `false`
|
||||
`OriginalData`: Will contain the originally received unparsed data if this has been enabled in the client options
|
||||
`Data`: Data returned by the server, only available if `Success` == `true`
|
||||
|
||||
When processing the result of a call it should always be checked for success. Not doing so will result in `NullReference` exceptions.
|
||||
When processing the result of a call it should always be checked for success. Not doing so will result in `NullReference` exceptions when the call fails for whatever reason.
|
||||
|
||||
*Check call result*
|
||||
```csharp
|
||||
@ -65,7 +72,7 @@ Console.WriteLine("Result: " + callResult.Data);
|
||||
```
|
||||
|
||||
## Socket client
|
||||
The socket client gives access to the websocket API of an exchange. Websocket API's offer streams to which updates are pushed to which a client can listen. Some exchanges also offer some degree of functionality by allowing clients to give commands via the websocket, but most exchanges only allow this via the Rest API.
|
||||
The socket client gives access to the websocket API of an exchange. Websocket API's offer streams to which updates are pushed to which a client can listen, and sometimes also allow request/response communication.
|
||||
Just like the Rest client is divided in Rest Api clients, the Socket client is divided into Socket Api clients, each with their own range of API functionality. Socket Api clients are generally not divided into topics since the number of methods isn't as big as with the Rest client. To use the Kucoin client as example again, it looks like this:
|
||||
|
||||
```csharp
|
||||
@ -80,7 +87,7 @@ Just like the Rest client is divided in Rest Api clients, the Socket client is d
|
||||
var subscribeResult = kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||
```
|
||||
|
||||
Subscribe methods require a data handler parameter, which is the method which will be called when an update is received from the server. This can be the name of a method or a lambda expression.
|
||||
Subscribe methods always require a data handler parameter, which is the method which will be called when an update is received from the server. This can be the name of a method or a lambda expression.
|
||||
|
||||
*Method reference*
|
||||
```csharp
|
||||
@ -100,12 +107,16 @@ await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData
|
||||
});
|
||||
```
|
||||
|
||||
All updates are wrapped in a `DataEvent<>` object, which contain a `Timestamp`, `OriginalData`, `Topic`, and a `Data` property. The `Timestamp` is the timestamp when the data was received (not send!). `OriginalData` will contain the originally received data if this has been enabled in the client options. `Topic` will contain the topic of the update, which is typically the symbol or asset the update is for. The `Data` property contains the received update data.
|
||||
All updates are wrapped in a `DataEvent<>` object, which contain the following properties:
|
||||
`Timestamp`: The timestamp when the data was received (not send!)
|
||||
`OriginalData`: Will contain the originally received unparsed data if this has been enabled in the client options
|
||||
`Topic`: Will contain the topic of the update, which is typically the symbol or asset the update is for
|
||||
`Data`: Contains the received update data.
|
||||
|
||||
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
|
||||
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient` without blocking the thread. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
|
||||
|
||||
### Processing subscribe responses
|
||||
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
||||
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as a [rest request](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
||||
```csharp
|
||||
|
||||
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||
@ -158,34 +169,3 @@ await kucoinSocketClient.UnsubscribeAsync(subscriptionResult.Data.Id);
|
||||
When you need to unsubscribe all current subscriptions on a client you can call `UnsubscribeAllAsync` on the client to unsubscribe all streams and close all connections.
|
||||
|
||||
|
||||
## Dependency injection
|
||||
Each library offers a `Add[Library]` extension method for `IServiceCollection`, which allows you to add the clients to the service collection. It also provides a callback for setting the client options. See this example for adding the `BinanceClient`:
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddBinance((restClientOptions, socketClientOptions) => {
|
||||
restClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
restClientOptions.LogLevel = LogLevel.Trace;
|
||||
|
||||
socketClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
});
|
||||
}
|
||||
```
|
||||
Doing client registration this way will add the `IBinanceClient` as a transient service, and the `IBinanceSocketClient` as a scoped service.
|
||||
|
||||
Alternatively, the clients can be registered manually:
|
||||
```csharp
|
||||
BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
|
||||
LogLevel = LogLevel.Trace
|
||||
});
|
||||
|
||||
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
|
||||
});
|
||||
|
||||
services.AddTransient<IBinanceClient, BinanceClient>();
|
||||
services.AddScoped<IBinanceSocketClient, BinanceSocketClient>();
|
||||
```
|
||||
|
15
docs/FAQ.md
15
docs/FAQ.md
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: FAQ
|
||||
nav_order: 11
|
||||
nav_order: 12
|
||||
---
|
||||
|
||||
## Frequently asked questions
|
||||
@ -48,18 +48,11 @@ private void SomeMethod()
|
||||
```
|
||||
|
||||
### Can I use the TestNet/US/other API with this library
|
||||
Yes, generally these are all supported and can be configured by setting the BaseAddress in the client options. Some known API addresses should be available in the [Exchange]ApiAddresses class. For example:
|
||||
Yes, generally these are all supported and can be configured by setting the Environment in the client options. Some known environments should be available in the [Exchange]Environment class. For example:
|
||||
```csharp
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
var client = new BinanceRestClient(options =>
|
||||
{
|
||||
SpotApiOptions = new BinanceApiClientOptions
|
||||
{
|
||||
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
|
||||
},
|
||||
UsdFuturesApiOptions = new BinanceApiClientOptions
|
||||
{
|
||||
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
||||
}
|
||||
options.Environment = BinanceEnvironment.Testnet;
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Glossary
|
||||
nav_order: 10
|
||||
nav_order: 11
|
||||
---
|
||||
## Terms and definitions
|
||||
|
||||
@ -18,7 +18,7 @@ nav_order: 10
|
||||
|Network|Chain|The network of an asset. For example `ETH` allows multiple networks like `ERC20` and `BEP2`|
|
||||
|Order book|Market depth|A list of (the top rows of) the current best bids and asks|
|
||||
|Ticker|Stats|Statistics over the last 24 hours|
|
||||
|Client implementation|Library|An implementation of the `CrytpoExchange.Net` library. For example `Binance.Net` or `FTX.Net`|
|
||||
|Client implementation|Library|An implementation of the `CrytpoExchange.Net` library. For example `Binance.Net` or `Bybit.Net`|
|
||||
|
||||
### Other naming conventions
|
||||
#### PlaceOrderAsync
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Common interfaces
|
||||
nav_order: 5
|
||||
nav_order: 7
|
||||
---
|
||||
|
||||
## Shared interfaces
|
||||
|
387
docs/Logging.md
387
docs/Logging.md
@ -1,320 +1,114 @@
|
||||
---
|
||||
title: Log config
|
||||
nav_order: 4
|
||||
title: Logging
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
## Configuring logging
|
||||
The library offers extensive logging, for which you can supply your own logging implementation. The logging can be configured via the client options (see [Client options](https://github.com/JKorf/CryptoExchange.Net/wiki/Options)). The examples here are using the `BinanceClient` but they should be the same for each implementation.
|
||||
The library offers extensive logging, which depends on the dotnet `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when connecting the library logging to your existing logging implementation.
|
||||
|
||||
Logging is based on the `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when connecting the library logging to your existing logging implementation.
|
||||
|
||||
## Serilog
|
||||
To make the CryptoExchange.Net logging write to the Serilog logger you can use the following methods, depending on the type of project you're using. The following examples assume that the `Serilog.Sinks.Console` package is already installed.
|
||||
|
||||
### Dotnet hosting
|
||||
|
||||
With for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client. Make sure to install the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore).
|
||||
|
||||
<Details>
|
||||
<Summary>
|
||||
Using ILogger injection
|
||||
|
||||
</Summary>
|
||||
<BlockQuote>
|
||||
Adding `UseSerilog()` in the `CreateHostBuilder` will add the Serilog logging implementation as an ILogger which you can inject into implementations.
|
||||
|
||||
*Configuring Serilog as ILogger:*
|
||||
*Configure logging to write to the console*
|
||||
```csharp
|
||||
IServiceCollection services = new ServiceCollection();
|
||||
services
|
||||
.AddBinance()
|
||||
.AddLogging(options =>
|
||||
{
|
||||
options.SetMinimumLevel(LogLevel.Trace);
|
||||
options.AddConsole();
|
||||
});
|
||||
```
|
||||
|
||||
public static void Main(string[] args)
|
||||
The library provides a TraceLogger ILogger implementation which writes log messages using `Trace.WriteLine`, but any other logging library can be used.
|
||||
|
||||
*Configure logging to use trace logging*
|
||||
```csharp
|
||||
IServiceCollection serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddBinance()
|
||||
.AddLogging(options =>
|
||||
{
|
||||
options.SetMinimumLevel(LogLevel.Trace);
|
||||
options.AddProvider(new TraceLoggerProvider());
|
||||
});
|
||||
```
|
||||
|
||||
### Using an external logging library and dotnet DI
|
||||
|
||||
With for example an ASP.Net Core or Blazor project the logging can be configured by the dependency container, which can then automatically be used be the clients.
|
||||
The next example shows how to use Serilog. This assumes the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore) is installed.
|
||||
|
||||
*Using serilog:*
|
||||
```csharp
|
||||
using Binance.Net;
|
||||
using Serilog;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddBinance();
|
||||
builder.Host.UseSerilog();
|
||||
var app = builder.Build();
|
||||
|
||||
// startup
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### Logging without dotnet DI
|
||||
If you don't have a dependency injection service available because you are for example working on a simple console application you have 2 options for logging.
|
||||
|
||||
#### Create a ServiceCollection manually and get the client from the service provider
|
||||
|
||||
```csharp
|
||||
IServiceCollection serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddBinance();
|
||||
serviceCollection.AddLogging(options =>
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
options.SetMinimumLevel(LogLevel.Trace);
|
||||
options.AddConsole();
|
||||
}).BuildServiceProvider();
|
||||
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
var client = serviceCollection.GetRequiredService<IBinanceRestClient>();
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseSerilog()
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
#### Create a LoggerFactory manually
|
||||
|
||||
*Injecting ILogger:*
|
||||
```csharp
|
||||
|
||||
public class BinanceDataProvider
|
||||
{
|
||||
BinanceClient _client;
|
||||
|
||||
public BinanceDataProvider(ILogger<BinanceDataProvider> logger)
|
||||
{
|
||||
_client = new BinanceClient(new BinanceClientOptions
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var logFactory = new LoggerFactory();
|
||||
logFactory.AddProvider(new ConsoleLoggerProvider());
|
||||
var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options => { });
|
||||
```
|
||||
|
||||
</BlockQuote>
|
||||
</Details>
|
||||
|
||||
<Details>
|
||||
<Summary>
|
||||
Using Add[Library] extension method
|
||||
|
||||
</Summary>
|
||||
<BlockQuote>
|
||||
When using the `Add[Library]` extension method, for instance `AddBinance()`, there is a small issue that there is no available `ILogger<>` yet when adding the library. This can be solved as follows:
|
||||
|
||||
*Configuring Serilog as ILogger:*
|
||||
```csharp
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup(
|
||||
context => new Startup(context.Configuration, LoggerFactory.Create(config => config.AddSerilog()) )); // <- this allows us to use ILoggerFactory in the Startup.cs
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
|
||||
*Injecting ILogger:*
|
||||
```csharp
|
||||
|
||||
public class Startup
|
||||
{
|
||||
private ILoggerFactory _loggerFactory;
|
||||
|
||||
public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
|
||||
{
|
||||
Configuration = configuration;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/* .. rest of class .. */
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddBinance((restClientOptions, socketClientOptions) => {
|
||||
// Point the logging to use the ILogger configuration
|
||||
restClientOptions.LogWriters = new List<ILogger> { _loggerFactory.CreateLogger<IBinanceClient>() };
|
||||
});
|
||||
|
||||
// Rest of service registrations
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</BlockQuote>
|
||||
</Details>
|
||||
|
||||
### Console application
|
||||
If you don't have a dependency injection service available because you are for example working on a simple console application you can use a slightly different approach.
|
||||
|
||||
*Configuring Serilog as ILogger:*
|
||||
```csharp
|
||||
var serilogLogger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
var loggerFactory = (ILoggerFactory)new LoggerFactory();
|
||||
loggerFactory.AddSerilog(serilogLogger);
|
||||
|
||||
```
|
||||
|
||||
*Injecting ILogger:*
|
||||
```csharp
|
||||
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
LogWriters = new List<ILogger> { loggerFactory.CreateLogger("") }
|
||||
});
|
||||
```
|
||||
|
||||
The `BinanceClient` will now write the logging it produces to the Serilog logger.
|
||||
|
||||
## Log4Net
|
||||
|
||||
To make the CryptoExchange.Net logging write to the Log4Net logge with for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client you're using. Make sure to install the `Microsoft.Extensions.Logging.Log4Net.AspNetCore` package (https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore).
|
||||
Adding `AddLog4Net()` in the `ConfigureLogging` call will add the Log4Net implementation as an ILogger which you can inject into implementations. Make sure you have a log4net.config configuration file in your project.
|
||||
|
||||
*Configuring Log4Net as ILogger:*
|
||||
```csharp
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.AddLog4Net();
|
||||
logging.SetMinimumLevel(LogLevel.Trace);
|
||||
});
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
```
|
||||
|
||||
*Injecting ILogger:*
|
||||
```csharp
|
||||
|
||||
public class BinanceDataProvider
|
||||
{
|
||||
BinanceClient _client;
|
||||
|
||||
public BinanceDataProvider(ILogger<BinanceDataProvider> logger)
|
||||
{
|
||||
_client = new BinanceClient(new BinanceClientOptions
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you don't have the Dotnet dependency container available you'll need to provide your own ILogger implementation. See [Custom logger](#custom-logger).
|
||||
|
||||
## NLog
|
||||
To make the CryptoExchange.Net logging write to the NLog logger you can use the following ways, depending on the type of project you're using.
|
||||
|
||||
### Dotnet hosting
|
||||
|
||||
With for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client you're using. Make sure to install the `NLog.Web.AspNetCore` package (https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-5).
|
||||
Adding `UseNLog()` to the `CreateHostBuilder()` method will add the NLog implementation as an ILogger which you can inject into implementations. Make sure you have a nlog.config configuration file in your project.
|
||||
|
||||
*Configuring NLog as ILogger:*
|
||||
```csharp
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.SetMinimumLevel(LogLevel.Trace);
|
||||
})
|
||||
.UseNLog();
|
||||
```
|
||||
|
||||
*Injecting ILogger:*
|
||||
```csharp
|
||||
|
||||
public class BinanceDataProvider
|
||||
{
|
||||
BinanceClient _client;
|
||||
|
||||
public BinanceDataProvider(ILogger<BinanceDataProvider> logger)
|
||||
{
|
||||
_client = new BinanceClient(new BinanceClientOptions
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you don't have the Dotnet dependency container available you'll need to provide your own ILogger implementation. See [Custom logger](#custom-logger).
|
||||
|
||||
## Custom logger
|
||||
If you're using a different framework or for some other reason these methods don't work for you you can create a custom ILogger implementation to receive the logging. All you need to do is create an implementation of the ILogger interface and provide that to the client.
|
||||
|
||||
*A simple console logging implementation (note that the ConsoleLogger is already available in the CryptoExchange.Net library)*:
|
||||
```csharp
|
||||
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Console.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
*Injecting the console logging implementation:*
|
||||
```csharp
|
||||
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
LogWriters = new List<ILogger> { new ConsoleLogger() }
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## Provide logging for issues
|
||||
## Providing logging for issues
|
||||
A big debugging tool when opening an issue on Github is providing logging of what data caused the issue. This can be provided two ways, via the `OriginalData` property of the call result or data event, or collecting the Trace logging.
|
||||
### OriginalData
|
||||
This is only useful when there is an issue in deserialization. So either a call result is giving a Deserialization error, or the result has a value that is unexpected. If that is the issue, please provide the original data that is received so the deserialization issue can be resolved based on the received data.
|
||||
By default the `OriginalData` property in the `WebCallResult`/`DataEvent` object is not filled as saving the original data has a (very small) performance penalty. To save the original data in the `OriginalData` property the `OutputOriginalData` option should be set to `true` in the client options.
|
||||
*Enabled output data*
|
||||
```csharp
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
var client = new BinanceClient(options =>
|
||||
{
|
||||
OutputOriginalData = true
|
||||
options.OutputOriginalData = true
|
||||
});
|
||||
```
|
||||
|
||||
*Accessing original data*
|
||||
```csharp
|
||||
// Rest request
|
||||
var tickerResult = client.SpotApi.ExchangeData.GetTickersAsync();
|
||||
var originallyRecievedData = tickerResult.OriginalData;
|
||||
var tickerResult = await client.SpotApi.ExchangeData.GetTickersAsync();
|
||||
var originallyReceivedData = tickerResult.OriginalData;
|
||||
|
||||
// Socket update
|
||||
client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
|
||||
await client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
|
||||
var originallyRecievedData = update.OriginalData;
|
||||
});
|
||||
```
|
||||
|
||||
### Trace logging
|
||||
Trace logging, which is the most verbose log level, can be enabled in the client options.
|
||||
*Enabled output data*
|
||||
```csharp
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
{
|
||||
LogLevel = LogLevel.Trace
|
||||
});
|
||||
```
|
||||
After enabling trace logging all data send to/received from the server is written to the log writers. By default this is written to the output window in Visual Studio via Debug.WriteLine, though this might be different depending on how you configured your logging.
|
||||
Trace logging, which is the most verbose log level, will show everything the library does and includes the data that was send and received.
|
||||
Output data will look something like this:
|
||||
```
|
||||
2021-12-17 10:40:42:296 | Debug | Binance | Client configuration: LogLevel: Trace, Writers: 1, OutputOriginalData: False, Proxy: -, AutoReconnect: True, ReconnectInterval: 00:00:05, MaxReconnectTries: , MaxResubscribeTries: 5, MaxConcurrentResubscriptionsPerSocket: 5, SocketResponseTimeout: 00:00:10, SocketNoDataTimeout: 00:00:00, SocketSubscriptionsCombineTarget: , CryptoExchange.Net: v5.0.0.0, Binance.Net: v8.0.0.0
|
||||
@ -323,3 +117,34 @@ Output data will look something like this:
|
||||
2021-12-17 10:40:43:024 | Debug | Binance | [15] Response received in 571ms: {"symbol":"BTCUSDT","priceChange":"-1726.47000000","priceChangePercent":"-3.531","weightedAvgPrice":"48061.51544204","prevClosePrice":"48901.44000000","lastPrice":"47174.97000000","lastQty":"0.00352000","bidPrice":"47174.96000000","bidQty":"0.65849000","askPrice":"47174.97000000","askQty":"0.13802000","openPrice":"48901.44000000","highPrice":"49436.43000000","lowPrice":"46749.55000000","volume":"33136.69765000","quoteVolume":"1592599905.80360790","openTime":1639647642763,"closeTime":1639734042763,"firstId":1191596486,"lastId":1192649611,"count":1053126}
|
||||
```
|
||||
When opening an issue, please provide this logging when available.
|
||||
|
||||
### Example of serilog config and minimal API's
|
||||
|
||||
```csharp
|
||||
using Binance.Net;
|
||||
using Binance.Net.Interfaces.Clients;
|
||||
using Serilog;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddBinance();
|
||||
builder.Host.UseSerilog();
|
||||
var app = builder.Build();
|
||||
|
||||
// startup
|
||||
|
||||
app.Urls.Add("http://localhost:3000");
|
||||
|
||||
app.MapGet("/price/{symbol}", async (string symbol) =>
|
||||
{
|
||||
var client = app.Services.GetRequiredService<IBinanceRestClient>();
|
||||
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
|
||||
return result.Data.Price;
|
||||
});
|
||||
|
||||
app.Run();
|
||||
```
|
@ -1,100 +1,73 @@
|
||||
---
|
||||
title: Migrate v4 to v5
|
||||
nav_order: 9
|
||||
title: Migrate v5 to v6
|
||||
nav_order: 10
|
||||
---
|
||||
|
||||
## Migrating from version 4 to version 5
|
||||
When updating your code from version 4 implementations to version 5 implementations you will encounter a fair bit of breaking changes. Here is the general outline for changes made in the CryptoExchange.Net library. For more specific changes for each library visit the library migration guide.
|
||||
## Migrating from version 5 to version 6
|
||||
When updating your code from version 5 implementations to version 6 implementations you will encounter some breaking changes. Here is the general outline of changes made in the CryptoExchange.Net library. For more specific changes for each library visit the library migration guide.
|
||||
|
||||
*NOTE when updating it is not possible to have some client implementations use a V4 version and some clients a V5. When updating all libraries should be migrated*
|
||||
*NOTE when updating it is not possible to have some client implementations use a V5 version and some clients a V6. When updating all libraries should be migrated*
|
||||
|
||||
## Client structure
|
||||
The client structure has been changed to make clients more consistent across different implementations. Clients using V4 either had `client.Method()`, `client.[Api].Method()` or `client.[Api].[Topic].Method()`.
|
||||
## Rest client name
|
||||
To be more clear about different clients for different API's the rest client implementations have been renamed from [Exchange]Client to [Exchange]RestClient. This makes it more clear that it only implements the Rest API and the [Exchange]SocketClient the Socket API.
|
||||
|
||||
This has been unified to be `client.[Api]Api.[Topic].Method()`:
|
||||
`bittrexClient.GetTickersAsync()` -> `bittrexClient.SpotApi.ExchangeData.GetTickersAsync()`
|
||||
`kucoinClient.Spot.GetTickersAsync()` -> `kucoinClient.SpotApi.ExchangeData.GetTickersAsync()`
|
||||
`binanceClient.Spot.Market.GetTickersAsync()` -> `binanceClient.SpotApi.ExchangeData.GetTickersAsync()`
|
||||
## Options
|
||||
Option parameters have been changed to a callback instead of an options object. This makes processing of the options easier and is in line with how dotnet handles option configurations.
|
||||
|
||||
Socket clients are restructured as `client.[Api]Streams.Method()`:
|
||||
`bittrexClient.SpotStreams.SubscribeToTickerUpdatesAsync()`
|
||||
`kucoinClient.SpotStreams.SubscribeToTickerUpdatesAsync()`
|
||||
`binanceClient.SpotStreams.SubscribeToAllTickerUpdatesAsync()`
|
||||
**BaseAddress**
|
||||
The BaseAddress option has been replaced by the Environment option. The Environment options allows for selection/switching between different trade environments more easily. For example the environment can be switched between a testnet and live by changing only a single line instead of having to change all BaseAddresses.
|
||||
|
||||
**LogLevel/LogWriters**
|
||||
The logging options have been removed and are now inherited by the DI configuration. See [Logging](https://jkorf.github.io/CryptoExchange.Net/Logging.html) for more info.
|
||||
|
||||
## Options structure
|
||||
The options have been changed in 2 categories, options for the whole client, and options only for a specific sub Api. Some options might no longer be available on the base level and should be set on the Api options instead, for example the `BaseAddress`.
|
||||
The following example sets some basic options, and specifically overwrites the USD futures Api options to use the test net address and different Api credentials:
|
||||
*V4*
|
||||
**HttpClient**
|
||||
The HttpClient will now be received by the DI container instead of having to pass it manually. When not using DI it is still possible to provide a HttpClient, but it is now located in the client constructor.
|
||||
|
||||
*V5*
|
||||
```csharp
|
||||
var binanceClient = new BinanceClient(new BinanceApiClientOptions{
|
||||
LogLevel = LogLevel.Trace,
|
||||
RequestTimeout = TimeSpan.FromSeconds(60),
|
||||
ApiCredentials = new ApiCredentials("API KEY", "API SECRET"),
|
||||
BaseAddressUsdtFutures = new ApiCredentials("OTHER API KEY ONLY FOR USD FUTURES", "OTHER API SECRET ONLY FOR USD FUTURES")
|
||||
// No way to set separate credentials for the futures API
|
||||
});
|
||||
```
|
||||
|
||||
*V5*
|
||||
```csharp
|
||||
var binanceClient = new BinanceClient(new BinanceClientOptions()
|
||||
{
|
||||
// Client options
|
||||
LogLevel = LogLevel.Trace,
|
||||
RequestTimeout = TimeSpan.FromSeconds(60),
|
||||
ApiCredentials = new ApiCredentials("API KEY", "API SECRET"),
|
||||
|
||||
// Set options specifically for the USD futures API
|
||||
UsdFuturesApiOptions = new BinanceApiClientOptions
|
||||
{
|
||||
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress,
|
||||
ApiCredentials = new ApiCredentials("OTHER API KEY ONLY FOR USD FUTURES", "OTHER API SECRET ONLY FOR USD FUTURES")
|
||||
var client = new BinanceClient(new BinanceClientOptions(){
|
||||
OutputOriginalData = true,
|
||||
SpotApiOptions = new RestApiOptions {
|
||||
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
|
||||
}
|
||||
// Other options
|
||||
});
|
||||
```
|
||||
See [Client options](https://github.com/JKorf/CryptoExchange.Net/wiki/Options) for more details on the specific options.
|
||||
|
||||
## IExchangeClient
|
||||
The `IExchangeClient` has been replaced by the `ISpotClient` and `IFuturesClient`. Where previously the `IExchangeClient` was implemented on the base client level, the `ISpotClient`/`IFuturesClient` have been implemented on the sub-Api level.
|
||||
This, in combination with the client restructuring, allows for more logically implemented interfaces, see this example:
|
||||
*V4*
|
||||
*V6*
|
||||
```csharp
|
||||
var spotClients = new [] {
|
||||
(IExhangeClient)binanceClient,
|
||||
(IExchangeClient)bittrexClient,
|
||||
(IExchangeClient)kucoinClient.Spot
|
||||
};
|
||||
|
||||
// There was no common implementation for futures client
|
||||
var client = new BinanceClient(options => {
|
||||
options.OutputOriginalData = true;
|
||||
options.Environment = BinanceEnvironment.Testnet;
|
||||
// Other options
|
||||
});
|
||||
```
|
||||
|
||||
*V5*
|
||||
```csharp
|
||||
var spotClients = new [] {
|
||||
binanceClient.SpotApi.CommonSpotClient,
|
||||
bittrexClient.SpotApi.CommonSpotClient,
|
||||
kucoinClient.SpotApi.CommonSpotClient
|
||||
};
|
||||
## Socket api name
|
||||
As socket API's are often more than just streams to subscribe to the name of the socket API clients have been changed from [Topic]Streams to [Topic]Api which matches the rest API client names. For example `SpotStreams` has become `SpotApi`, so `binanceSocketClient.UsdFuturesStreams.SubscribeXXX` has become `binanceSocketClient.UsdFuturesApi.SubscribeXXX`.
|
||||
|
||||
var futuresClients = new [] {
|
||||
binanceClient.UsdFuturesApi.CommonFuturesClient,
|
||||
kucoinClient.FuturesApi.CommonFuturesClient
|
||||
};
|
||||
## Add[Exchange] extension method
|
||||
With the change in options providing the DI extension methods for the IServiceCollection have also been changed slightly. Also the socket clients will now be registered as Singleton by default instead of Scoped.
|
||||
|
||||
*V5*
|
||||
```csharp
|
||||
builder.Services.AddKucoin((restOpts, socketOpts) =>
|
||||
{
|
||||
restOpts.LogLevel = LogLevel.Debug;
|
||||
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
socketOpts.LogLevel = LogLevel.Debug;
|
||||
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
}, ServiceLifetime.Singleton);
|
||||
```
|
||||
|
||||
Where the IExchangeClient was returning interfaces which were implemented by models from the exchange, the `ISpotClient`/`IFuturesClient` returns actual objects defined in the `CryptoExchange.Net` library. This shifts the responsibility of parsing
|
||||
the library model to a shared model from the model class to the client class, which makes more sense and removes the need for separate library models to implement the same mapping logic. It also removes the need for the `Common` prefix on properties:
|
||||
*V4*
|
||||
*V6*
|
||||
```csharp
|
||||
var kline = await ((IExhangeClient)binanceClient).GetKlinesAysnc(/*params*/);
|
||||
var closePrice = kline.CommonClose;
|
||||
```
|
||||
|
||||
*V5*
|
||||
```csharp
|
||||
var kline = await binanceClient.SpotApi.ComonSpotClient.GetKlinesAysnc(/*params*/);
|
||||
var closePrice = kline.ClosePrice;
|
||||
```
|
||||
|
||||
For more details on the interfaces see [Common interfaces](interfaces.html)
|
||||
builder.Services.AddKucoin((restOpts) =>
|
||||
{
|
||||
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
},
|
||||
(socketOpts) =>
|
||||
{
|
||||
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
});
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Client options
|
||||
nav_order: 3
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
## Setting client options
|
||||
@ -10,10 +10,10 @@ Each implementation can be configured using client options. There are 2 ways to
|
||||
*Set the default options to use for new clients*
|
||||
```csharp
|
||||
|
||||
BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
||||
BinanceClient.SetDefaultOptions(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET")
|
||||
options.OutputOriginalData = true;
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
});
|
||||
|
||||
```
|
||||
@ -21,10 +21,10 @@ BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
||||
*Set the options to use for a single new client*
|
||||
```csharp
|
||||
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
var client = new BinanceClient(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET")
|
||||
options.OutputOriginalData = true;
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
});
|
||||
|
||||
```
|
||||
@ -32,39 +32,30 @@ var client = new BinanceClient(new BinanceClientOptions
|
||||
When calling `SetDefaultOptions` each client created after that will use the options that were set, unless the specific option is overriden in the options that were provided to the client. Consider the following example:
|
||||
```csharp
|
||||
|
||||
BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
||||
BinanceClient.SetDefaultOptions(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
OutputOriginalData = true
|
||||
options.OutputOriginalData = true;
|
||||
});
|
||||
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
var client = new BinanceClient(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Debug,
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET")
|
||||
options.OutputOriginalData = false;
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
The client instance will have the following options:
|
||||
`LogLevel = Debug`
|
||||
`OutputOriginalData = true`
|
||||
`ApiCredentials = set`
|
||||
`OutputOriginalData = false`
|
||||
|
||||
## Api options
|
||||
The options are divided in two categories. The basic options, which will apply to everything the client does, and the Api options, which is limited to the specific API client (see [Clients](https://github.com/JKorf/CryptoExchange.Net/wiki/Clients)).
|
||||
The options are divided in two categories. The basic options, which will apply to everything the client does, and the Api options, which is limited to the specific API client (see [Clients](https://jkorf.github.io/CryptoExchange.Net/Clients.html)).
|
||||
|
||||
```csharp
|
||||
|
||||
var client = new BinanceClient(new BinanceClientOptions
|
||||
var client = new BinanceRestClient(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Debug,
|
||||
ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
|
||||
SpotApiOptions = new BinanceApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET") ,
|
||||
BaseAddress = BinanceApiAddresses.Us.RestClientAddress
|
||||
}
|
||||
options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
|
||||
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||
});
|
||||
|
||||
```
|
||||
@ -78,38 +69,39 @@ All clients have access to the following options, specific implementations might
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`LogWriters`| A list of `ILogger`s to handle log messages. | `new List<ILogger> { new DebugLogger() }` |
|
||||
|`LogLevel`| The minimum log level before passing messages to the `LogWriters`. Messages with a more verbose level than the one specified here will be ignored. Setting this to `null` will pass all messages to the `LogWriters`.| `LogLevel.Information`
|
||||
|`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`
|
||||
|`ApiCredentials`| The API credentials to use for accessing protected endpoints. Typically a key/secret combination. Note that this is a `default` value for all API clients, and can be overridden per API client. See the `Base Api client options`| `null`
|
||||
|`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). Note that this is a `default` value for all API clients, and can be overridden per API client. See the `Base Api client options`| `null`
|
||||
|`Proxy`|The proxy to use for connecting to the API.| `null`
|
||||
|`RequestTimeout`|The timeout for client requests to the server| `TimeSpan.FromSeconds(20)`
|
||||
|
||||
**Rest client options (extension of base client options)**
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`RequestTimeout`|The time out to use for requests.|`TimeSpan.FromSeconds(30)`|
|
||||
|`HttpClient`|The `HttpClient` instance to use for making requests. When creating multiple `RestClient` instances a single `HttpClient` should be provided to prevent each client instance from creating its own. *[WARNING] When providing the `HttpClient` instance in the options both the `RequestTimeout` and `Proxy` client options will be ignored and should be set on the provided `HttpClient` instance.*| `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 sure to be in sync.|`true`|
|
||||
|`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed| `TimeSpan.FromHours(1)`
|
||||
|`Environment`|The environment the library should talk to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments|`Live environment`
|
||||
|
||||
**Socket client options (extension of base client options)**
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`AutoReconnect`|Whether or not the socket should automatically reconnect when disconnected.|`true`
|
||||
|`AutoReconnect`|Whether or not the socket should attempt to automatically reconnect when disconnected.|`true`
|
||||
|`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)`
|
||||
|`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)`
|
||||
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout)
|
||||
|`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket. Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection, potentially leading to issues.| Depends on implementation
|
||||
|`MaxReconnectTries`|The maximum amount of tries for reconnecting|`null` (infinite)
|
||||
|`MaxResubscribeTries`|The maximum amount of tries for resubscribing after successfully reconnecting the socket|5
|
||||
|`MaxConcurrentResubscriptionsPerSocket`|The maximum number of concurrent resubscriptions per socket when resubscribing after reconnecting|5
|
||||
|`MaxSocketConnections`|The maximum amount of distinct socket connections|`null`
|
||||
|`DelayAfterConnect`|The time to wait before sending messages after connecting to the server.|`TimeSpan.Zero`
|
||||
|`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`
|
||||
|
||||
**Base Api client options**
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`ApiCredentials`|The API credentials to use for this specific API client. Will override any credentials provided in the base client options|
|
||||
|`BaseAddress`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example. Available base addresses are defined in the [Library]ApiAddresses helper class, for example `KucoinApiAddresses`|Depends on implementation
|
||||
|`ApiCredentials`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update. Overrides the Base client options `OutputOriginalData` option if set| `false`
|
||||
|`OutputOriginalData`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example. Available base addresses are defined in the [Library]ApiAddresses helper class, for example `KucoinApiAddresses`|Depends on implementation
|
||||
|
||||
**Options for Rest Api Client (extension of base api client options)**
|
||||
|
||||
@ -117,6 +109,21 @@ All clients have access to the following options, specific implementations might
|
||||
|------|-----------|-------|
|
||||
|`RateLimiters`|A list of `IRateLimiter`s to use.|`new List<IRateLimiter>()`|
|
||||
|`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`|
|
||||
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time sure to be in sync. Overrides the Rest client options `AutoTimestamp` option if set|`null`|
|
||||
|`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)**
|
||||
There are currently no specific options for socket API clients, the base API options are still available.
|
||||
|
||||
|Option|Description|Default|
|
||||
|------|-----------|-------|
|
||||
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. Overrides the Socket client options `SocketNoDataTimeout` option if set | `default(TimeSpan)` (no timeout)
|
||||
|`MaxSocketConnections`|The maximum amount of distinct socket connections. Overrides the Socket client options `MaxSocketConnections` option if set |`null`
|
||||
|
||||
## Credentials
|
||||
Credentials are supported in 3 formats in the base library:
|
||||
|
||||
|Type|Description|Example|
|
||||
|----|-----------|-------|
|
||||
|`Hmac`|An API key + secret combination. The API key is send with the request and the secret is used to sign requests. This is the default authentication method on all exchanges. |`options.ApiCredentials = new ApiCredentials("51231f76e-9c503548-8fabs3f-rfgf12mkl3", "556be32-d563ba53-faa2dfd-b3n5c", CredentialType.Hmac);`|
|
||||
|`RsaPem`|An API key + a public and private key pair generated by the user. The public key is shared with the exchange, while the private key is used to sign requests. This CredentialType expects the private key to be in .pem format and is only supported in .netstandard2.1 due to limitations of the framework|`options.ApiCredentials = new ApiCredentials("432vpV8daAaXAF4Qg", ""-----BEGIN PRIVATE KEY-----[PRIVATEKEY]-----END PRIVATE KEY-----", CredentialType.RsaPem);`|
|
||||
|`RsaXml`|An API key + a public and private key pair generated by the user. The public key is shared with the exchange, while the private key is used to sign requests. This CredentialType expects the private key to be in xml format and is supported in .netstandard2.0 and .netstandard2.1, but it might mean the private key needs to be converted from the original format to xml|`options.ApiCredentials = new ApiCredentials("432vpV8daAaXAF4Qg", "<RSAKeyValue>[PRIVATEKEY]</RSAKeyValue>", CredentialType.RsaXml);`|
|
@ -4,11 +4,11 @@ nav_order: 6
|
||||
---
|
||||
|
||||
## Locally synced order book
|
||||
Each implementation provides an order book implementation. These implementations will provide a client side order book and will take care of synchronization with the server, and will handle reconnecting and resynchronizing in case of a dropped connection.
|
||||
Each exchange implementation provides an order book implementation. These implementations will provide a client side order book and will take care of synchronization with the server, and will handle reconnecting and resynchronizing in case of a dropped connection.
|
||||
Order book implementations are named as `[ExchangeName][Type]SymbolOrderBook`, for example `BinanceSpotSymbolOrderBook`.
|
||||
|
||||
## Usage
|
||||
Start the book synchronization by calling the `StartAsync` method. This returns a success state whether the book is successfully synchronized and started. You can listen to the `OnStatusChange` event to be notified of when the status of a book changes. Note that the order book is only synchronized with the server when the state is `Synced`.
|
||||
Start the book synchronization by calling the `StartAsync` method. This returns whether the book is successfully synchronized and started. You can listen to the `OnStatusChange` event to be notified of when the status of a book changes. Note that the order book is only synchronized with the server when the state is `Synced`. When the order book has been started and the state changes from `Synced` to `Reconnecting` the book will automatically reconnect and resync itself.
|
||||
|
||||
*Start an order book and print the top 3 rows*
|
||||
```csharp
|
||||
@ -24,6 +24,7 @@ if (!startResult.Success)
|
||||
|
||||
while(true)
|
||||
{
|
||||
Console.Clear();
|
||||
Console.WriteLine(book.ToString(3);
|
||||
await Task.Delay(500);
|
||||
}
|
||||
@ -54,4 +55,14 @@ book.OnStatusChange += (oldStatus, newStatus) => { Console.WriteLine($"State cha
|
||||
book.OnOrderBookUpdate += (bidsAsks) => { Console.WriteLine($"Order book changed: {bidsAsks.Asks.Count()} asks, {bidsAsks.Bids.Count()} bids"); };
|
||||
book.OnBestOffersChanged += (bestOffer) => { Console.WriteLine($"Best offer changed, best bid: {bestOffer.BestBid.Price}, best ask: {bestOffer.BestAsk.Price}"); };
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
### Order book factory
|
||||
Each exchange implementation also provides an order book factory for creating ISymbolOrderBook instances. The naming convention for the factory is `[Exchange]OrderBookFactory`, for example `BinanceOrderBookFactory`. This type will be automatically added when using DI and can be used to facilitate easier testing.
|
||||
|
||||
*Creating an order book using the order book factory*
|
||||
```csharp
|
||||
var factory = services.GetRequiredService<IKucoinOrderBookFactory>();
|
||||
var book = factory.CreateSpot("ETH-USDT");
|
||||
var startResult = await book.StartAsync();
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Rate limiting
|
||||
nav_order: 7
|
||||
nav_order: 9
|
||||
---
|
||||
|
||||
## Rate limiting
|
||||
|
@ -17,7 +17,6 @@ These will always be on the latest CryptoExchange.Net version and the latest ver
|
||||
|<a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Bittrex.Net/Icon/icon.png?raw=true"></a>|Bittrex|https://jkorf.github.io/Bittrex.Net/|
|
||||
|<a href="https://github.com/JKorf/Bybit.Net"><img src="https://github.com/JKorf/Bybit.Net/blob/main/ByBit.Net/Icon/icon.png?raw=true"></a>|Bybit|https://jkorf.github.io/Bybit.Net/|
|
||||
|<a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/CoinEx.Net/Icon/icon.png?raw=true"></a>|CoinEx|https://jkorf.github.io/CoinEx.Net/|
|
||||
|<a href="https://github.com/JKorf/FTX.Net"><img src="https://github.com/JKorf/FTX.Net/blob/main/FTX.Net/Icon/icon.png?raw=true"></a>|FTX|https://jkorf.github.io/FTX.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/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/|
|
||||
@ -52,16 +51,14 @@ Use one of the following following referral links to signup to a new exchange to
|
||||
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||
[FTX](https://ftx.com/referrals#a=31620192)
|
||||
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||
|
||||
### Donate
|
||||
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||
|
||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||
**Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
|
||||
**Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
|
||||
|
||||
### Sponsor
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Loading…
x
Reference in New Issue
Block a user