diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index db589fd..b968417 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -7,7 +7,7 @@ CryptoExchange.Net JKorf - 0.0.36 + 0.0.37 false https://github.com/JKorf/CryptoExchange.Net https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE diff --git a/CryptoExchange.Net/ExchangeClient.cs b/CryptoExchange.Net/ExchangeClient.cs index 47e28d1..0714e14 100644 --- a/CryptoExchange.Net/ExchangeClient.cs +++ b/CryptoExchange.Net/ExchangeClient.cs @@ -3,12 +3,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; using System.Reflection; using System.Threading.Tasks; using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; using CryptoExchange.Net.Requests; using Newtonsoft.Json; @@ -20,8 +23,10 @@ namespace CryptoExchange.Net { public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); + protected string baseAddress; protected Log log; protected ApiProxy apiProxy; + protected RateLimitingBehaviour rateLimitBehaviour; protected AuthenticationProvider authProvider; private List rateLimiters; @@ -47,10 +52,12 @@ namespace CryptoExchange.Net log.UpdateWriters(exchangeOptions.LogWriters); log.Level = exchangeOptions.LogVerbosity; + baseAddress = exchangeOptions.BaseAddress; apiProxy = exchangeOptions.Proxy; if (apiProxy != null) log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}"); + rateLimitBehaviour = exchangeOptions.RateLimitingBehaviour; rateLimiters = new List(); foreach (var rateLimiter in exchangeOptions.RateLimiters) rateLimiters.Add(rateLimiter); @@ -83,11 +90,49 @@ namespace CryptoExchange.Net authProvider = authentictationProvider; } + /// + /// Ping to see if the server is reachable + /// + /// The roundtrip time of the ping request + public CallResult Ping() => PingAsync().Result; + + /// + /// Ping to see if the server is reachable + /// + /// The roundtrip time of the ping request + public async Task> PingAsync() + { + var ping = new Ping(); + var uri = new Uri(baseAddress); + PingReply reply = null; + try + { + reply = await ping.SendPingAsync(uri.Host); + } + catch(PingException e) + { + if(e.InnerException != null) + { + if (e.InnerException is SocketException) + return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + ((SocketException)e.InnerException).SocketErrorCode }); + else + return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + e.InnerException.Message }); + } + return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + e.Message }); + } + if (reply.Status == IPStatus.Success) + return new CallResult(reply.RoundtripTime, null); + return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + reply.Status }); + } + protected virtual async Task> ExecuteRequest(Uri uri, string method = "GET", Dictionary parameters = null, bool signed = false) where T : class { log.Write(LogVerbosity.Debug, $"Creating request for " + uri); if (signed && authProvider == null) + { + log.Write(LogVerbosity.Warning, $"Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); return new CallResult(null, new NoApiCredentialsError()); + } var request = ConstructRequest(uri, method, parameters, signed); @@ -99,9 +144,14 @@ namespace CryptoExchange.Net foreach (var limiter in rateLimiters) { - var limitedBy = limiter.LimitRequest(uri.AbsolutePath); - if (limitedBy > 0) - log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitedBy}ms by {limiter.GetType().Name}"); + var limitResult = limiter.LimitRequest(uri.AbsolutePath, rateLimitBehaviour); + if (!limitResult.Success) + { + log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} failed because of rate limit"); + return new CallResult(null, limitResult.Error); + } + else if (limitResult.Data > 0) + log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); } string paramString = null; diff --git a/CryptoExchange.Net/ApiProxy.cs b/CryptoExchange.Net/Objects/ApiProxy.cs similarity index 100% rename from CryptoExchange.Net/ApiProxy.cs rename to CryptoExchange.Net/Objects/ApiProxy.cs diff --git a/CryptoExchange.Net/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs similarity index 100% rename from CryptoExchange.Net/CallResult.cs rename to CryptoExchange.Net/Objects/CallResult.cs diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs new file mode 100644 index 0000000..ba0f75c --- /dev/null +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -0,0 +1,8 @@ +namespace CryptoExchange.Net.Objects +{ + public enum RateLimitingBehaviour + { + Fail, + Wait + } +} diff --git a/CryptoExchange.Net/Error.cs b/CryptoExchange.Net/Objects/Error.cs similarity index 75% rename from CryptoExchange.Net/Error.cs rename to CryptoExchange.Net/Objects/Error.cs index a136407..2d5be84 100644 --- a/CryptoExchange.Net/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -38,21 +38,26 @@ public class WebError : Error { - public WebError(string message) : base(3, "Web error: " + message) { } + public WebError(string message) : base(4, "Web error: " + message) { } } public class DeserializeError : Error { - public DeserializeError(string message) : base(4, "Error deserializing data: " + message) { } + public DeserializeError(string message) : base(5, "Error deserializing data: " + message) { } } public class UnknownError : Error { - public UnknownError(string message) : base(5, "Unknown error occured " + message) { } + public UnknownError(string message) : base(6, "Unknown error occured " + message) { } } public class ArgumentError : Error { - public ArgumentError(string message) : base(5, "Invalid parameter: " + message) { } + public ArgumentError(string message) : base(7, "Invalid parameter: " + message) { } + } + + public class RateLimitError: Error + { + public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { } } } diff --git a/CryptoExchange.Net/ExchangeOptions.cs b/CryptoExchange.Net/Objects/ExchangeOptions.cs similarity index 73% rename from CryptoExchange.Net/ExchangeOptions.cs rename to CryptoExchange.Net/Objects/ExchangeOptions.cs index b2629da..67d3159 100644 --- a/CryptoExchange.Net/ExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/ExchangeOptions.cs @@ -2,6 +2,7 @@ using System.IO; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; namespace CryptoExchange.Net @@ -17,6 +18,11 @@ namespace CryptoExchange.Net /// public ApiCredentials ApiCredentials { get; set; } + /// + /// The base address of the client + /// + public string BaseAddress { get; set; } + /// /// Proxy to use /// @@ -36,5 +42,10 @@ namespace CryptoExchange.Net /// List of ratelimiters to use /// public List RateLimiters { get; set; } = new List(); + + /// + /// What to do when a call would exceed the rate limit + /// + public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; } } diff --git a/CryptoExchange.Net/RateLimiter/IRateLimiter.cs b/CryptoExchange.Net/RateLimiter/IRateLimiter.cs index 4a1d5c9..507ecd0 100644 --- a/CryptoExchange.Net/RateLimiter/IRateLimiter.cs +++ b/CryptoExchange.Net/RateLimiter/IRateLimiter.cs @@ -1,7 +1,9 @@ -namespace CryptoExchange.Net.RateLimiter +using CryptoExchange.Net.Objects; + +namespace CryptoExchange.Net.RateLimiter { public interface IRateLimiter { - double LimitRequest(string url); + CallResult LimitRequest(string url, RateLimitingBehaviour limitBehaviour); } } diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs b/CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs index 60e56f4..9ef576e 100644 --- a/CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs +++ b/CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs @@ -1,4 +1,5 @@ -using System; +using CryptoExchange.Net.Objects; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; @@ -27,7 +28,7 @@ namespace CryptoExchange.Net.RateLimiter this.perTimePeriod = perTimePeriod; } - public double LimitRequest(string url) + public CallResult LimitRequest(string url, RateLimitingBehaviour limitingBehaviour) { int waitTime; RateLimitObject rlo; @@ -49,6 +50,9 @@ namespace CryptoExchange.Net.RateLimiter waitTime = rlo.GetWaitTime(DateTime.UtcNow, limitPerEndpoint, perTimePeriod); if (waitTime != 0) { + if(limitingBehaviour == RateLimitingBehaviour.Fail) + return new CallResult(waitTime, new RateLimitError($"endpoint limit of {limitPerEndpoint} reached on endpoint " + url)); + Thread.Sleep(Convert.ToInt32(waitTime)); waitTime += (int)sw.ElapsedMilliseconds; } @@ -56,7 +60,7 @@ namespace CryptoExchange.Net.RateLimiter rlo.Add(DateTime.UtcNow); } - return waitTime; + return new CallResult(waitTime, null); } } } diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs b/CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs index 683c2fd..d087685 100644 --- a/CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs +++ b/CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs @@ -1,4 +1,5 @@ -using System; +using CryptoExchange.Net.Objects; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -28,7 +29,7 @@ namespace CryptoExchange.Net.RateLimiter this.perTimePeriod = perTimePeriod; } - public double LimitRequest(string url) + public CallResult LimitRequest(string url, RateLimitingBehaviour limitBehaviour) { var sw = Stopwatch.StartNew(); lock (requestLock) @@ -43,6 +44,9 @@ namespace CryptoExchange.Net.RateLimiter waitTime = (history.First() - (checkTime - perTimePeriod)).TotalMilliseconds; if (waitTime > 0) { + if (limitBehaviour == RateLimitingBehaviour.Fail) + return new CallResult(waitTime, new RateLimitError($"total limit of {limit} reached")); + Thread.Sleep(Convert.ToInt32(waitTime)); waitTime += sw.ElapsedMilliseconds; } @@ -50,7 +54,7 @@ namespace CryptoExchange.Net.RateLimiter history.Add(DateTime.UtcNow); history.Sort(); - return waitTime; + return new CallResult(waitTime, null); } } }