diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..eba1110
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3d05591
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: csharp
+mono: none
+solution: CryptoExchange.Net.sln
+dotnet: 2.0.0
+dist: trusty
+script:
+ - dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.0"
+ - dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj
\ No newline at end of file
diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj
new file mode 100644
index 0000000..28c7213
--- /dev/null
+++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CryptoExchange.Net.UnitTests/ExchangeClientTests.cs b/CryptoExchange.Net.UnitTests/ExchangeClientTests.cs
new file mode 100644
index 0000000..b9be9e6
--- /dev/null
+++ b/CryptoExchange.Net.UnitTests/ExchangeClientTests.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using CryptoExchange.Net.Authentication;
+using CryptoExchange.Net.Interfaces;
+using CryptoExchange.Net.Logging;
+using CryptoExchange.Net.RateLimiter;
+using Moq;
+using Newtonsoft.Json;
+using NUnit.Framework;
+
+namespace CryptoExchange.Net.UnitTests
+{
+ [TestFixture()]
+ public class ExchangeClientTests
+ {
+ [TestCase(null, null)]
+ [TestCase("", "")]
+ [TestCase("test", null)]
+ [TestCase("test", "")]
+ [TestCase(null, "test")]
+ [TestCase("", "test")]
+ public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
+ {
+ // arrange
+ var client = PrepareClient("");
+
+ // act
+ // assert
+ Assert.Throws(typeof(ArgumentException), () => client.SetApiCredentails(key, secret));
+ }
+
+ [TestCase()]
+ public void SettingLogOutput_Should_RedirectLogOutput()
+ {
+ // arrange
+ var stringBuilder = new StringBuilder();
+ var client = PrepareClient("{}", true, LogVerbosity.Debug, new StringWriter(stringBuilder));
+
+ // act
+ client.TestCall();
+
+ // assert
+ Assert.IsFalse(string.IsNullOrEmpty(stringBuilder.ToString()));
+ }
+
+ [TestCase()]
+ public void ObjectDeserializationFail_Should_GiveFailedResult()
+ {
+ // arrange
+ var errorMessage = "TestErrorMessage";
+ var client = PrepareClient(JsonConvert.SerializeObject(errorMessage));
+
+ // act
+ var result = client.TestCall();
+
+ // assert
+ Assert.IsFalse(result.Success);
+ Assert.AreNotEqual(0, result.Error.Code);
+ Assert.IsTrue(result.Error.Message.Contains(errorMessage));
+ }
+
+ [TestCase()]
+ public void InvalidJson_Should_GiveFailedResult()
+ {
+ // arrange
+ var errorMessage = "TestErrorMessage";
+ var client = PrepareClient(JsonConvert.SerializeObject(errorMessage));
+
+ // act
+ var result = client.TestCall();
+
+ // assert
+ Assert.IsFalse(result.Success);
+ Assert.AreNotEqual(0, result.Error.Code);
+ Assert.IsTrue(result.Error.Message.Contains(errorMessage));
+ }
+
+ [TestCase()]
+ public void WhenUsingRateLimiterTotalRequests_Should_BeDelayed()
+ {
+ // arrange
+ var client = PrepareClient(JsonConvert.SerializeObject(new TestObject()));
+ client.AddRateLimiter(new RateLimiterTotal(1, TimeSpan.FromSeconds(5)));
+
+ // act
+ var sw = Stopwatch.StartNew();
+ client.TestCall();
+ client.TestCall();
+ client.TestCall();
+ sw.Stop();
+
+ // assert
+ Assert.IsTrue(sw.ElapsedMilliseconds > 9000);
+ }
+
+ [TestCase()]
+ public void WhenUsingRateLimiterPerEndpointRequests_Should_BeDelayed()
+ {
+ // arrange
+ var client = PrepareClient(JsonConvert.SerializeObject(new TestObject()));
+ client.AddRateLimiter(new RateLimiterTotal(1, TimeSpan.FromSeconds(5)));
+
+ // act
+ var sw = Stopwatch.StartNew();
+ client.TestCall();
+ client.TestCall();
+ client.TestCall();
+ sw.Stop();
+
+ // assert
+ Assert.IsTrue(sw.ElapsedMilliseconds > 9000);
+ }
+
+ [TestCase()]
+ public void WhenRemovingRateLimiterRequest_Should_NoLongerBeDelayed()
+ {
+ // arrange
+ var client = PrepareClient(JsonConvert.SerializeObject(new TestObject()));
+ client.AddRateLimiter(new RateLimiterTotal(1, TimeSpan.FromSeconds(5)));
+ client.RemoveRateLimiters();
+
+ // act
+ var sw = Stopwatch.StartNew();
+ client.TestCall();
+ client.TestCall();
+ client.TestCall();
+ sw.Stop();
+
+ // assert
+ Assert.IsTrue(sw.ElapsedMilliseconds < 5000);
+ }
+
+ [TestCase()]
+ public void ReceivingErrorStatusCode_Should_NotSuccess()
+ {
+ // arrange
+ var client = PrepareExceptionClient(JsonConvert.SerializeObject(new TestObject()), "InvalidStatusCodeResponse", 203);
+
+ // act
+ var result = client.TestCall();
+
+ // assert
+ Assert.IsFalse(result.Success);
+ Assert.IsNotNull(result.Error);
+ Assert.IsTrue(result.Error.Message.Contains("InvalidStatusCodeResponse"));
+ }
+
+ private TestImplementation PrepareClient(string responseData, bool withOptions = true, LogVerbosity verbosity = LogVerbosity.Warning, TextWriter tw = null)
+ {
+ var expectedBytes = Encoding.UTF8.GetBytes(responseData);
+ var responseStream = new MemoryStream();
+ responseStream.Write(expectedBytes, 0, expectedBytes.Length);
+ responseStream.Seek(0, SeekOrigin.Begin);
+
+ var response = new Mock();
+ response.Setup(c => c.GetResponseStream()).Returns(responseStream);
+
+ var request = new Mock();
+ request.Setup(c => c.Headers).Returns(new WebHeaderCollection());
+ request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
+ request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object));
+
+ var factory = new Mock();
+ factory.Setup(c => c.Create(It.IsAny()))
+ .Returns(request.Object);
+ TestImplementation client;
+ if (withOptions)
+ {
+ client = new TestImplementation(new ExchangeOptions()
+ {
+ ApiCredentials = new ApiCredentials("Test", "Test2"),
+ LogVerbosity = verbosity,
+ LogWriter = tw
+ });
+ }
+ else
+ {
+ client = new TestImplementation();
+ }
+ client.RequestFactory = factory.Object;
+ return client;
+ }
+
+ private TestImplementation PrepareExceptionClient(string responseData, string exceptionMessage, int statusCode, bool credentials = true)
+ {
+ var expectedBytes = Encoding.UTF8.GetBytes(responseData);
+ var responseStream = new MemoryStream();
+ responseStream.Write(expectedBytes, 0, expectedBytes.Length);
+ responseStream.Seek(0, SeekOrigin.Begin);
+
+ var we = new WebException();
+ var r = new HttpWebResponse();
+ var re = new HttpResponseMessage();
+
+ typeof(HttpResponseMessage).GetField("_statusCode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(re, (HttpStatusCode)statusCode);
+ typeof(HttpWebResponse).GetField("_httpResponseMessage", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(r, re);
+ typeof(WebException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, exceptionMessage);
+ typeof(WebException).GetField("_response", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, r);
+
+ var response = new Mock();
+ response.Setup(c => c.GetResponseStream()).Throws(we);
+
+ var request = new Mock();
+ request.Setup(c => c.Headers).Returns(new WebHeaderCollection());
+ request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object));
+
+ var factory = new Mock();
+ factory.Setup(c => c.Create(It.IsAny()))
+ .Returns(request.Object);
+
+ TestImplementation client = credentials ? new TestImplementation(new ExchangeOptions() { ApiCredentials = new ApiCredentials("Test", "Test2") }) : new TestImplementation();
+ client.RequestFactory = factory.Object;
+ return client;
+ }
+ }
+}
diff --git a/CryptoExchange.Net.UnitTests/TestImplementation.cs b/CryptoExchange.Net.UnitTests/TestImplementation.cs
new file mode 100644
index 0000000..b4f75eb
--- /dev/null
+++ b/CryptoExchange.Net.UnitTests/TestImplementation.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using CryptoExchange.Net.Authentication;
+using CryptoExchange.Net.Interfaces;
+
+namespace CryptoExchange.Net.UnitTests
+{
+ public class TestImplementation: ExchangeClient
+ {
+ public TestImplementation(): base(new ExchangeOptions(), null) { }
+
+ public TestImplementation(ExchangeOptions exchangeOptions) : base(exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
+ {
+ }
+
+ public void SetApiCredentails(string key, string secret)
+ {
+ SetAuthenticationProvider(new TestAuthProvider(new ApiCredentials(key, secret)));
+ }
+
+ public CallResult TestCall()
+ {
+ return ExecuteRequest(new Uri("http://www.test.com")).Result;
+ }
+ }
+
+ public class TestAuthProvider : AuthenticationProvider
+ {
+ public TestAuthProvider(ApiCredentials credentials) : base(credentials)
+ {
+ }
+
+ public override string AddAuthenticationToUriString(string uri, bool signed)
+ {
+ return uri;
+ }
+
+ public override IRequest AddAuthenticationToRequest(IRequest request, bool signed)
+ {
+ return request;
+ }
+
+ public override string Sign(string toSign)
+ {
+ return toSign;
+ }
+ }
+
+ public class TestObject
+ {
+ public int Id { get; set; }
+ public List Data { get; set; }
+ }
+}
diff --git a/CryptoExchange.Net.sln b/CryptoExchange.Net.sln
new file mode 100644
index 0000000..0f431bb
--- /dev/null
+++ b/CryptoExchange.Net.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27004.2008
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net.UnitTests", "CryptoExchange.Net.UnitTests\CryptoExchange.Net.UnitTests.csproj", "{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
+ EndGlobalSection
+EndGlobal
diff --git a/ApiProxy.cs b/CryptoExchange.Net/ApiProxy.cs
similarity index 100%
rename from ApiProxy.cs
rename to CryptoExchange.Net/ApiProxy.cs
diff --git a/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs
similarity index 100%
rename from Authentication/ApiCredentials.cs
rename to CryptoExchange.Net/Authentication/ApiCredentials.cs
diff --git a/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs
similarity index 100%
rename from Authentication/AuthenticationProvider.cs
rename to CryptoExchange.Net/Authentication/AuthenticationProvider.cs
diff --git a/BaseConverter.cs b/CryptoExchange.Net/BaseConverter.cs
similarity index 100%
rename from BaseConverter.cs
rename to CryptoExchange.Net/BaseConverter.cs
diff --git a/CallResult.cs b/CryptoExchange.Net/CallResult.cs
similarity index 100%
rename from CallResult.cs
rename to CryptoExchange.Net/CallResult.cs
diff --git a/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj
similarity index 100%
rename from CryptoExchange.Net.csproj
rename to CryptoExchange.Net/CryptoExchange.Net.csproj
diff --git a/Error.cs b/CryptoExchange.Net/Error.cs
similarity index 100%
rename from Error.cs
rename to CryptoExchange.Net/Error.cs
diff --git a/ExchangeClient.cs b/CryptoExchange.Net/ExchangeClient.cs
similarity index 100%
rename from ExchangeClient.cs
rename to CryptoExchange.Net/ExchangeClient.cs
diff --git a/ExchangeOptions.cs b/CryptoExchange.Net/ExchangeOptions.cs
similarity index 100%
rename from ExchangeOptions.cs
rename to CryptoExchange.Net/ExchangeOptions.cs
diff --git a/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs
similarity index 100%
rename from ExtensionMethods.cs
rename to CryptoExchange.Net/ExtensionMethods.cs
diff --git a/Interfaces/IExchangeClient.cs b/CryptoExchange.Net/Interfaces/IExchangeClient.cs
similarity index 100%
rename from Interfaces/IExchangeClient.cs
rename to CryptoExchange.Net/Interfaces/IExchangeClient.cs
diff --git a/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs
similarity index 100%
rename from Interfaces/IRequest.cs
rename to CryptoExchange.Net/Interfaces/IRequest.cs
diff --git a/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs
similarity index 100%
rename from Interfaces/IRequestFactory.cs
rename to CryptoExchange.Net/Interfaces/IRequestFactory.cs
diff --git a/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs
similarity index 100%
rename from Interfaces/IResponse.cs
rename to CryptoExchange.Net/Interfaces/IResponse.cs
diff --git a/Logging/DebugTextWriter.cs b/CryptoExchange.Net/Logging/DebugTextWriter.cs
similarity index 100%
rename from Logging/DebugTextWriter.cs
rename to CryptoExchange.Net/Logging/DebugTextWriter.cs
diff --git a/Logging/Log.cs b/CryptoExchange.Net/Logging/Log.cs
similarity index 100%
rename from Logging/Log.cs
rename to CryptoExchange.Net/Logging/Log.cs
diff --git a/RateLimiter/IRateLimiter.cs b/CryptoExchange.Net/RateLimiter/IRateLimiter.cs
similarity index 100%
rename from RateLimiter/IRateLimiter.cs
rename to CryptoExchange.Net/RateLimiter/IRateLimiter.cs
diff --git a/RateLimiter/RateLimitObject.cs b/CryptoExchange.Net/RateLimiter/RateLimitObject.cs
similarity index 100%
rename from RateLimiter/RateLimitObject.cs
rename to CryptoExchange.Net/RateLimiter/RateLimitObject.cs
diff --git a/RateLimiter/RateLimiterPerEndpoint.cs b/CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs
similarity index 100%
rename from RateLimiter/RateLimiterPerEndpoint.cs
rename to CryptoExchange.Net/RateLimiter/RateLimiterPerEndpoint.cs
diff --git a/RateLimiter/RateLimiterTotal.cs b/CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs
similarity index 100%
rename from RateLimiter/RateLimiterTotal.cs
rename to CryptoExchange.Net/RateLimiter/RateLimiterTotal.cs
diff --git a/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs
similarity index 100%
rename from Requests/Request.cs
rename to CryptoExchange.Net/Requests/Request.cs
diff --git a/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs
similarity index 100%
rename from Requests/RequestFactory.cs
rename to CryptoExchange.Net/Requests/RequestFactory.cs
diff --git a/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs
similarity index 100%
rename from Requests/Response.cs
rename to CryptoExchange.Net/Requests/Response.cs