diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index cd28063..d171427 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -166,5 +166,37 @@ namespace CryptoExchange.Net.UnitTests Assert.IsTrue(result2.Success); Assert.IsTrue(sw.ElapsedMilliseconds > 900, $"Actual: {sw.ElapsedMilliseconds}"); } + + [TestCase] + public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey() + { + // arrange + var client = new TestRestClient(new ClientOptions() + { + RateLimiters = new List { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) }, + RateLimitingBehaviour = RateLimitingBehaviour.Wait, + LogVerbosity = LogVerbosity.Debug, + ApiCredentials = new ApiCredentials("TestKey", "TestSecret") + }); + client.SetResponse("{\"property\": 123}"); + + + // act + var sw = Stopwatch.StartNew(); + var result1 = client.Request().Result; + client.SetKey("TestKey2", "TestSecret2"); // set to different key + client.SetResponse("{\"property\": 123}"); // reset response stream + var result2 = client.Request().Result; + client.SetKey("TestKey", "TestSecret"); // set back to original key, should delay + client.SetResponse("{\"property\": 123}"); // reset response stream + var result3 = client.Request().Result; + sw.Stop(); + + // assert + Assert.IsTrue(result1.Success); + Assert.IsTrue(result2.Success); + Assert.IsTrue(result3.Success); + Assert.IsTrue(sw.ElapsedMilliseconds > 900 && sw.ElapsedMilliseconds < 1900, $"Actual: {sw.ElapsedMilliseconds}"); + } } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 351f0ba..f9e8782 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Reflection; using System.Text; using System.Threading.Tasks; +using CryptoExchange.Net.Authentication; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -25,6 +26,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations RequestFactory = new Mock().Object; } + public void SetKey(string key, string secret) + { + SetAuthenticationProvider(new UnitTests.TestAuthProvider(new ApiCredentials(key, secret))); + } + public void SetResponse(string responseData, Stream requestStream = null) { var expectedBytes = Encoding.UTF8.GetBytes(responseData); @@ -92,6 +98,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } } + public class TestAuthProvider : AuthenticationProvider + { + public TestAuthProvider(ApiCredentials credentials) : base(credentials) + { + } + } + public class ParseErrorTestRestClient: TestRestClient { public ParseErrorTestRestClient() { } diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 3bbd751..f99db21 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -17,7 +17,7 @@ namespace CryptoExchange.Net public string BaseAddress { get; private set; } protected internal Log log; protected ApiProxy apiProxy; - protected AuthenticationProvider authProvider; + protected internal AuthenticationProvider authProvider; protected static int lastId; protected static object idLock = new object(); diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs new file mode 100644 index 0000000..b686345 --- /dev/null +++ b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; + +namespace CryptoExchange.Net.RateLimiter +{ + /// + /// Limits the amount of requests per time period to a certain limit, counts the request per API key. + /// + public class RateLimiterAPIKey: IRateLimiter + { + internal Dictionary history = new Dictionary(); + + private readonly int limitPerKey; + private readonly TimeSpan perTimePeriod; + private readonly object historyLock = new object(); + + /// + /// Create a new RateLimiterAPIKey. This rate limiter limits the amount of requests per time period to a certain limit, counts the request per API key. + /// + /// The amount to limit to + /// The time period over which the limit counts + public RateLimiterAPIKey(int limitPerApiKey, TimeSpan perTimePeriod) + { + limitPerKey = limitPerApiKey; + this.perTimePeriod = perTimePeriod; + } + + + public CallResult LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour) + { + if(client.authProvider?.Credentials == null) + return new CallResult(0, null); + + string key = client.authProvider.Credentials.Key.GetString(); + + int waitTime; + RateLimitObject rlo; + lock (historyLock) + { + if (history.ContainsKey(key)) + rlo = history[key]; + else + { + rlo = new RateLimitObject(); + history.Add(key, rlo); + } + } + + var sw = Stopwatch.StartNew(); + lock (rlo.LockObject) + { + sw.Stop(); + waitTime = rlo.GetWaitTime(DateTime.UtcNow, limitPerKey, perTimePeriod); + if (waitTime != 0) + { + if (limitBehaviour == RateLimitingBehaviour.Fail) + return new CallResult(waitTime, new RateLimitError($"endpoint limit of {limitPerKey} reached on api key " + key)); + + Thread.Sleep(Convert.ToInt32(waitTime)); + waitTime += (int)sw.ElapsedMilliseconds; + } + + rlo.Add(DateTime.UtcNow); + } + + return new CallResult(waitTime, null); + } + } +}