From 1d5bb8a817f4c748e1f34aef7d9796d397aa40c0 Mon Sep 17 00:00:00 2001 From: Mike Pearl Date: Tue, 26 Aug 2025 22:47:07 -0700 Subject: [PATCH] Update clients to use SecretApiKey authentication. --- .../SecretApiKeyAuthenticatorTests.cs | 47 + .../CoinbaseAdvancedTradeApiClientTests.cs | 9 +- ...inbaseAdvancedTradeClient.UnitTests.csproj | 8 +- ...inbaseAdvancedTradeWebSocketClientTests.cs | 348 +- .../Endpoints/AccountsEndpointTests.cs | 1008 +++-- .../Endpoints/OrdersEndpointTests.cs | 3346 ++++++++--------- .../Endpoints/ProductsEndpointTests.cs | 2140 ++++++----- .../TransactionSummaryEndpointTests.cs | 8 +- .../TestHelpers/TestConfigHelper.cs | 36 + .../SecretApiKeyAuthenticator.cs | 108 + .../CoinbaseAdvancedTradeApiClient.cs | 42 +- .../CoinbaseAdvancedTradeClient.csproj | 5 +- .../CoinbaseAdvancedTradeWebsocketClient.cs | 430 +-- .../Endpoints/AccountsEndpoint.cs | 132 +- .../Endpoints/OrdersEndpoint.cs | 518 ++- .../Endpoints/ProductsEndpoint.cs | 255 +- .../Endpoints/TransactionSummaryEndpoint.cs | 90 +- .../Models/Config/SecretApiKeyConfig.cs | 11 + .../Config/SecretApiKeyWebSocketConfig.cs | 11 + .../Models/WebSocket/SubscriptionMessage.cs | 46 +- .../Resources/ErrorMessages.Designer.cs | 603 +-- .../Resources/ErrorMessages.resx | 397 +- 22 files changed, 4921 insertions(+), 4677 deletions(-) create mode 100644 CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Authentication/SecretApiKeyAuthenticatorTests.cs create mode 100644 CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/TestHelpers/TestConfigHelper.cs create mode 100644 CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Authentication/SecretApiKeyAuthenticator.cs create mode 100644 CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyConfig.cs create mode 100644 CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyWebSocketConfig.cs diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Authentication/SecretApiKeyAuthenticatorTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Authentication/SecretApiKeyAuthenticatorTests.cs new file mode 100644 index 0000000..aafe20d --- /dev/null +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Authentication/SecretApiKeyAuthenticatorTests.cs @@ -0,0 +1,47 @@ +using CoinbaseAdvancedTradeClient.Authentication; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; +using Xunit; + +namespace CoinbaseAdvancedTradeClient.UnitTests.Authentication +{ + public class SecretApiKeyAuthenticatorTests + { + [Fact] + public void GenerateBearerJWT_ValidParameters_ReturnsJWT() + { + // Arrange + var testKeySecret = TestConfigHelper.GenerateTestKeySecret(); + + // Act & Assert - Should not throw exception + var jwt = SecretApiKeyAuthenticator.GenerateBearerJWT( + "test-key-name", + testKeySecret, + "GET", + "api.coinbase.com", + "/v1/test" + ); + + // Basic JWT format validation + Assert.NotNull(jwt); + Assert.Contains(".", jwt); + var parts = jwt.Split('.'); + Assert.Equal(3, parts.Length); // header.payload.signature + } + + [Fact] + public void GenerateBearerJWT_InvalidKeySecret_ThrowsArgumentException() + { + // Act & Assert + Assert.Throws(() => + { + SecretApiKeyAuthenticator.GenerateBearerJWT( + "test-key-name", + "invalid-key-format", + "GET", + "api.coinbase.com", + "/v1/test" + ); + }); + } + } +} \ No newline at end of file diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeApiClientTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeApiClientTests.cs index 0faba69..47e2194 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeApiClientTests.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeApiClientTests.cs @@ -1,4 +1,5 @@ using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; using Xunit; namespace CoinbaseAdvancedTradeClient.UnitTests @@ -9,7 +10,7 @@ public class CoinbaseAdvancedTradeApiClientTests public void Constructor_NullConfig_ThrowsArgumentNullException() { //Arrange - ApiClientConfig config = null; + SecretApiKeyConfig config = null; //Act & Assert Assert.Throws(() => @@ -27,10 +28,10 @@ public void Constructor_NullConfig_ThrowsArgumentNullException() public void Constructor_EmptyConfigSetting_ThrowsArgumentException(string key, string secret) { //Arrange - ApiClientConfig config = new ApiClientConfig() + SecretApiKeyConfig config = new SecretApiKeyConfig() { - ApiKey = key, - ApiSecret = secret + KeyName = key, + KeySecret = string.IsNullOrWhiteSpace(secret) ? secret : TestConfigHelper.GenerateTestKeySecret() }; //Act & Assert diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeClient.UnitTests.csproj b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeClient.UnitTests.csproj index 209fd69..b4f7742 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeClient.UnitTests.csproj +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeClient.UnitTests.csproj @@ -8,10 +8,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeWebSocketClientTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeWebSocketClientTests.cs index ff7c831..47871e0 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeWebSocketClientTests.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/CoinbaseAdvancedTradeWebSocketClientTests.cs @@ -1,176 +1,172 @@ -using CoinbaseAdvancedTradeClient.Constants; -using CoinbaseAdvancedTradeClient.Interfaces; -using CoinbaseAdvancedTradeClient.Models.Config; -using System.Net.Sockets; -using Xunit; - -namespace CoinbaseAdvancedTradeClient.UnitTests -{ - public class CoinbaseAdvancedTradeWebSocketClientTests - { - [Fact] - public void Constructor_NullConfig_ThrowsArgumentNullException() - { - //Arrange - WebSocketClientConfig config = null; - - //Act & Assert - Assert.Throws(() => - { - var result = new CoinbaseAdvancedTradeWebSocketClient(config); - }); - } - - [Theory] - [InlineData("", "")] - [InlineData("Test", "")] - [InlineData("", "Test")] - [InlineData(" ", "Test")] - [InlineData("Test", " ")] - public void Constructor_EmptyConfigSetting_ThrowsArgumentException(string key, string secret) - { - //Arrange - WebSocketClientConfig config = new WebSocketClientConfig() - { - ApiKey = key, - ApiSecret = secret - }; - - //Act & Assert - Assert.Throws(() => - { - var result = new CoinbaseAdvancedTradeWebSocketClient(config); - }); - } - - #region Connection - - [Fact] - public async Task ConnectAsync_NullMessageReceivedCallback_ThrowsArgumentNullException() - { - //Arrange - var testClient = CreateTestClient(); - - //Act & Assert - await Assert.ThrowsAsync(async () => - { - var result = await testClient.ConnectAsync(null); - }); - } - - #endregion // Connection - - #region Subscription - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("TEST")] - public void Subscribe_InvalidChannel_ThrowsArgumentException(string channel) - { - //Arrange - var productIds = new List { "BTC-USD" }; - - var testClient = CreateTestClient(); - - //Act & Assert - Assert.Throws(() => - { - testClient.Subscribe(channel, productIds); - }); - } - - [Fact] - public void Subscribe_NullProductIds_ThrowsArgumentNullException() - { - //Arrange - var channel = WebSocketChannels.Ticker; - - var testClient = CreateTestClient(); - - //Act & Assert - Assert.Throws(() => - { - testClient.Subscribe(channel, null); - }); - } - - [Fact] - public void Subscribe_EmptyProductIds_ThrowsArgumentNullException() - { - //Arrange - var channel = WebSocketChannels.Ticker; - var productIds = new List(); - - var testClient = CreateTestClient(); - - //Act & Assert - Assert.Throws(() => - { - testClient.Subscribe(channel, productIds); - }); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("TEST")] - public void Unsubscribe_InvalidChannel_ThrowsArgumentException(string channel) - { - //Arrange - var productIds = new List { "BTC-USD" }; - - var testClient = CreateTestClient(); - - //Act & Assert - Assert.Throws(() => - { - testClient.Unsubscribe(channel, productIds); - }); - } - - [Fact] - public void Unsubscribe_NullProductIds_ThrowsArgumentNullException() - { - //Arrange - var channel = WebSocketChannels.Ticker; - - var testClient = CreateTestClient(); - - //Act & Assert - Assert.Throws(() => - { - testClient.Unsubscribe(channel, null); - }); - } - - [Fact] - public void Unsubscribe_EmptyProductIds_ThrowsArgumentNullException() - { - //Arrange - var channel = WebSocketChannels.Ticker; - var productIds = new List(); - - var testClient = CreateTestClient(); - - //Act & Assert - Assert.Throws(() => - { - testClient.Unsubscribe(channel, productIds); - }); - } - - #endregion // Subscription - - private ICoinbaseAdvancedTradeWebSocketClient CreateTestClient() - { - WebSocketClientConfig config = new WebSocketClientConfig() - { - ApiKey = "testKey", - ApiSecret = "testSecret" - }; - - return new CoinbaseAdvancedTradeWebSocketClient(config); - } - } -} +using CoinbaseAdvancedTradeClient.Constants; +using CoinbaseAdvancedTradeClient.Interfaces; +using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; +using System.Net.Sockets; +using Xunit; + +namespace CoinbaseAdvancedTradeClient.UnitTests +{ + public class CoinbaseAdvancedTradeWebSocketClientTests + { + [Fact] + public void Constructor_NullConfig_ThrowsArgumentNullException() + { + //Arrange + SecretApiKeyWebSocketConfig config = null; + + //Act & Assert + Assert.Throws(() => + { + var result = new CoinbaseAdvancedTradeWebSocketClient(config); + }); + } + + [Theory] + [InlineData("", "")] + [InlineData("Test", "")] + [InlineData("", "Test")] + [InlineData(" ", "Test")] + [InlineData("Test", " ")] + public void Constructor_EmptyConfigSetting_ThrowsArgumentException(string key, string secret) + { + //Arrange + SecretApiKeyWebSocketConfig config = new SecretApiKeyWebSocketConfig() + { + KeyName = key, + KeySecret = string.IsNullOrWhiteSpace(secret) ? secret : TestConfigHelper.GenerateTestKeySecret() + }; + + //Act & Assert + Assert.Throws(() => + { + var result = new CoinbaseAdvancedTradeWebSocketClient(config); + }); + } + + #region Connection + + [Fact] + public async Task ConnectAsync_NullMessageReceivedCallback_ThrowsArgumentNullException() + { + //Arrange + var testClient = CreateTestClient(); + + //Act & Assert + await Assert.ThrowsAsync(async () => + { + var result = await testClient.ConnectAsync(null); + }); + } + + #endregion // Connection + + #region Subscription + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("TEST")] + public void Subscribe_InvalidChannel_ThrowsArgumentException(string channel) + { + //Arrange + var productIds = new List { "BTC-USD" }; + + var testClient = CreateTestClient(); + + //Act & Assert + Assert.Throws(() => + { + testClient.Subscribe(channel, productIds); + }); + } + + [Fact] + public void Subscribe_NullProductIds_ThrowsArgumentNullException() + { + //Arrange + var channel = WebSocketChannels.Ticker; + + var testClient = CreateTestClient(); + + //Act & Assert + Assert.Throws(() => + { + testClient.Subscribe(channel, null); + }); + } + + [Fact] + public void Subscribe_EmptyProductIds_ThrowsArgumentNullException() + { + //Arrange + var channel = WebSocketChannels.Ticker; + var productIds = new List(); + + var testClient = CreateTestClient(); + + //Act & Assert + Assert.Throws(() => + { + testClient.Subscribe(channel, productIds); + }); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("TEST")] + public void Unsubscribe_InvalidChannel_ThrowsArgumentException(string channel) + { + //Arrange + var productIds = new List { "BTC-USD" }; + + var testClient = CreateTestClient(); + + //Act & Assert + Assert.Throws(() => + { + testClient.Unsubscribe(channel, productIds); + }); + } + + [Fact] + public void Unsubscribe_NullProductIds_ThrowsArgumentNullException() + { + //Arrange + var channel = WebSocketChannels.Ticker; + + var testClient = CreateTestClient(); + + //Act & Assert + Assert.Throws(() => + { + testClient.Unsubscribe(channel, null); + }); + } + + [Fact] + public void Unsubscribe_EmptyProductIds_ThrowsArgumentNullException() + { + //Arrange + var channel = WebSocketChannels.Ticker; + var productIds = new List(); + + var testClient = CreateTestClient(); + + //Act & Assert + Assert.Throws(() => + { + testClient.Unsubscribe(channel, productIds); + }); + } + + #endregion // Subscription + + private ICoinbaseAdvancedTradeWebSocketClient CreateTestClient() + { + var config = TestConfigHelper.CreateTestWebSocketConfig(); + return new CoinbaseAdvancedTradeWebSocketClient(config); + } + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/AccountsEndpointTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/AccountsEndpointTests.cs index 6fa6ea5..2f964da 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/AccountsEndpointTests.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/AccountsEndpointTests.cs @@ -1,506 +1,502 @@ -using CoinbaseAdvancedTradeClient.Interfaces; -using CoinbaseAdvancedTradeClient.Models.Api.Accounts; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Config; -using CoinbaseAdvancedTradeClient.Models.Pages; -using Flurl.Http; -using Flurl.Http.Testing; -using System.Globalization; -using Xunit; - -namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints -{ - public class AccountsEndpointTests - { - private readonly ICoinbaseAdvancedTradeApiClient _testClient; - - public AccountsEndpointTests() - { - var config = new ApiClientConfig() - { - ApiKey = "key", - ApiSecret = "secret" - }; - - _testClient = new CoinbaseAdvancedTradeApiClient(config); - } - - #region GetListAccountsAsync - - [Fact] - public async Task GetListAccountsAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var accountsListJson = GetAccountsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountsListJson); - - result = await _testClient.Accounts.GetListAccountsAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetListAccountsAsync_ValidRequestAndResponseJson_ResponseHasValidAccountsPage() - { - //Arrange - ApiResponse result; - - var accountsListJson = GetAccountsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountsListJson); - - result = await _testClient.Accounts.GetListAccountsAsync(); - } - - //Assert - Assert.Null(result.Data.Account); - Assert.NotNull(result.Data.Accounts); - Assert.Equal(2, result.Data.Size); - Assert.True(result.Data.HasNext); - Assert.Equal("789100", result.Data.Cursor); - } - - [Fact] - public async Task GetListAccountsAsync_ValidRequestAndResponseJson_ResponseHasValidAccounts() - { - //Arrange - ApiResponse result; - - var accountsListJson = GetAccountsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountsListJson); - - result = await _testClient.Accounts.GetListAccountsAsync(); - } - - //Assert - Assert.NotNull(result.Data.Accounts); - Assert.Contains(result.Data.Accounts, a => a.Currency.Equals("BTC", StringComparison.InvariantCultureIgnoreCase)); - Assert.Contains(result.Data.Accounts, a => a.Currency.Equals("ETH", StringComparison.InvariantCultureIgnoreCase)); - } - - [Fact] - public async Task GetListAccountsAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var invalidJson = GetInvalildAccountsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(invalidJson); - - result = await _testClient.Accounts.GetListAccountsAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - - [Theory] - [InlineData(251)] - [InlineData(0)] - [InlineData(-25)] - public async Task GetListAccountsAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) - { - //Arrange - ApiResponse result; - - var accountsListJson = GetAccountsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountsListJson); - - result = await _testClient.Accounts.GetListAccountsAsync(limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task GetListAccountsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var accountsListJson = GetAccountsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Accounts.GetListAccountsAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetListAccountsAsync - - #region GetAccountAsync - - [Fact] - public async Task GetAccountAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var accountId = "test"; - var accountJson = GetAccountJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountJson); - - result = await _testClient.Accounts.GetAccountAsync(accountId); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetAccountsAsync_ValidRequestAndResponseJson_ResponseHasValidAccountValues() - { - //Arrange - ApiResponse result; - - var accountId = "test"; - var accountJson = GetAccountJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountJson); - - result = await _testClient.Accounts.GetAccountAsync(accountId); - } - - //Assert - Assert.NotNull(result.Data); - Assert.Equal("8bfc20d7-f7c6-4422-bf07-8243ca4169fe", result.Data.Id); - Assert.Equal("BTC Wallet", result.Data.Name); - Assert.Equal("BTC", result.Data.Currency); - Assert.Equal(1.23m, result.Data.AvailableBalance.Value); - Assert.Equal("BTC", result.Data.AvailableBalance.Currency); - Assert.False(result.Data.Default); - Assert.True(result.Data.Active); - Assert.Equal("2021-05-31T09:59:59", result.Data.CreatedAt.Value.ToString("s", new CultureInfo("en-US"))); - Assert.Equal("2021-05-31T09:59:59", result.Data.UpdatedAt.Value.ToString("s", new CultureInfo("en-US"))); - Assert.Equal("2021-05-31T09:59:59", result.Data.DeletedAt.Value.ToString("s", new CultureInfo("en-US"))); - Assert.Equal("ACCOUNT_TYPE_UNSPECIFIED", result.Data.Type); - Assert.True(result.Data.Ready); - Assert.Equal(1.23m, result.Data.Hold.Value); - Assert.Equal("BTC", result.Data.Hold.Currency); - } - - [Fact] - public async Task GetAccountAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var accountId = "test"; - var invalidJson = GetInvalidAccountJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(invalidJson); - - result = await _testClient.Accounts.GetAccountAsync(accountId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(" ")] - [InlineData("\t")] - [InlineData(null)] - public async Task GetAccountAsync_NullOrWhitespaceAccountId_ReturnsUnsuccessfulApiResponse(string accountId) - { - //Arrange - ApiResponse result; - - var accountJson = GetAccountJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(accountJson); - - result = await _testClient.Accounts.GetAccountAsync(accountId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task GetAccountAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var accountId = "test"; - var accountJson = GetAccountJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Accounts.GetAccountAsync(accountId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetAccountAsync - - #region Test Response Json - - private string GetAccountsListJsonString() - { - var json = - """ - { - "accounts": [ - { - "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", - "name": "BTC Wallet", - "currency": "BTC", - "available_balance": { - "value": "1.23", - "currency": "BTC" - }, - "default": false, - "active": true, - "created_at": "2021-05-31T09:59:59Z", - "updated_at": "2021-05-31T09:59:59Z", - "deleted_at": "2021-05-31T09:59:59Z", - "type": "ACCOUNT_TYPE_UNSPECIFIED", - "ready": true, - "hold": { - "value": "1.23", - "currency": "BTC" - } - }, - { - "uuid": "9bfc20d7-e7c5-3421-af06-9243ca4169ff", - "name": "ETH Wallet", - "currency": "ETH", - "available_balance": { - "value": "1.23", - "currency": "ETH" - }, - "default": false, - "active": true, - "created_at": "2021-05-31T09:59:59Z", - "updated_at": "2021-05-31T09:59:59Z", - "deleted_at": "2021-05-31T09:59:59Z", - "type": "ACCOUNT_TYPE_UNSPECIFIED", - "ready": true, - "hold": { - "value": "1.23", - "currency": "ETH" - } - } - ], - "has_next": true, - "cursor": "789100", - "size": "2" - } - """; - - return json; - } - - - private string GetInvalildAccountsListJsonString() - { - var json = - """ - { - "accounts": [ - { - "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", - "name": "BTC Wallet", - "currency": "BTC", - "available_balance": { - "value": "1.23", - "currency": "BTC" - }, - "default": false, - "active": true, - "created_at": "2021-05-31T09:59:59Z", - "updated_at": "2021-05-31T09:59:59Z", - "deleted_at": "2021-05-31T09:59:59Z", - "type": "ACCOUNT_TYPE_UNSPECIFIED", - "ready": true, - "hold": { - "value": "1.23", - "currency": "BTC" - } - }, - { - "uuid": "9bfc20d7-e7c5-3421-af06-9243ca4169ff", - "name": "ETH Wallet", - "currency": "ETH", - "available_balance": { - "value": "1.23", - "currency": "ETH" - }, - "default": false, - "active": true, - "created_at": "2021-05-31T09:59:59Z", - "updated_at": "2021-05-31T09:59:59Z", - "deleted_at": "2021-05-31T09:59:59Z", - "type": "ACCOUNT_TYPE_UNSPECIFIED", - "ready": true, - "hold": { - "value": "1.23", - "currency": "ETH" - } - } - ], - "has_next": true, - "cursor": "789100", - "size": "invalid" - } - """; - - return json; - } - - private string GetAccountJsonString() - { - var json = - """ - { - "account": { - "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", - "name": "BTC Wallet", - "currency": "BTC", - "available_balance": { - "value": "1.23", - "currency": "BTC" - }, - "default": false, - "active": true, - "created_at": "2021-05-31T09:59:59Z", - "updated_at": "2021-05-31T09:59:59Z", - "deleted_at": "2021-05-31T09:59:59Z", - "type": "ACCOUNT_TYPE_UNSPECIFIED", - "ready": true, - "hold": { - "value": "1.23", - "currency": "BTC" - } - } - } - """; - - return json; - } - - private string GetInvalidAccountJsonString() - { - var json = - """ - { - "account": { - "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", - "name": "BTC Wallet", - "currency": "BTC", - "available_balance": { - "value": "1.23", - "currency": "BTC" - }, - "default": false, - "active": "invalid", - "created_at": "2021-05-31T09:59:59Z", - "updated_at": "2021-05-31T09:59:59Z", - "deleted_at": "2021-05-31T09:59:59Z", - "type": "ACCOUNT_TYPE_UNSPECIFIED", - "ready": true, - "hold": { - "value": "1.23", - "currency": "BTC" - } - } - } - """; - - return json; - } - - #endregion // Test Response Json - } -} +using CoinbaseAdvancedTradeClient.Interfaces; +using CoinbaseAdvancedTradeClient.Models.Api.Accounts; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.Models.Pages; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; +using Flurl.Http; +using Flurl.Http.Testing; +using System.Globalization; +using Xunit; + +namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints +{ + public class AccountsEndpointTests + { + private readonly ICoinbaseAdvancedTradeApiClient _testClient; + + public AccountsEndpointTests() + { + var config = TestConfigHelper.CreateTestApiConfig(); + _testClient = new CoinbaseAdvancedTradeApiClient(config); + } + + #region GetListAccountsAsync + + [Fact] + public async Task GetListAccountsAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var accountsListJson = GetAccountsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountsListJson); + + result = await _testClient.Accounts.GetListAccountsAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetListAccountsAsync_ValidRequestAndResponseJson_ResponseHasValidAccountsPage() + { + //Arrange + ApiResponse result; + + var accountsListJson = GetAccountsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountsListJson); + + result = await _testClient.Accounts.GetListAccountsAsync(); + } + + //Assert + Assert.Null(result.Data.Account); + Assert.NotNull(result.Data.Accounts); + Assert.Equal(2, result.Data.Size); + Assert.True(result.Data.HasNext); + Assert.Equal("789100", result.Data.Cursor); + } + + [Fact] + public async Task GetListAccountsAsync_ValidRequestAndResponseJson_ResponseHasValidAccounts() + { + //Arrange + ApiResponse result; + + var accountsListJson = GetAccountsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountsListJson); + + result = await _testClient.Accounts.GetListAccountsAsync(); + } + + //Assert + Assert.NotNull(result.Data.Accounts); + Assert.Contains(result.Data.Accounts, a => a.Currency.Equals("BTC", StringComparison.InvariantCultureIgnoreCase)); + Assert.Contains(result.Data.Accounts, a => a.Currency.Equals("ETH", StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task GetListAccountsAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var invalidJson = GetInvalildAccountsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(invalidJson); + + result = await _testClient.Accounts.GetListAccountsAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + + [Theory] + [InlineData(251)] + [InlineData(0)] + [InlineData(-25)] + public async Task GetListAccountsAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) + { + //Arrange + ApiResponse result; + + var accountsListJson = GetAccountsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountsListJson); + + result = await _testClient.Accounts.GetListAccountsAsync(limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task GetListAccountsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var accountsListJson = GetAccountsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Accounts.GetListAccountsAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetListAccountsAsync + + #region GetAccountAsync + + [Fact] + public async Task GetAccountAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var accountId = "test"; + var accountJson = GetAccountJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountJson); + + result = await _testClient.Accounts.GetAccountAsync(accountId); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetAccountsAsync_ValidRequestAndResponseJson_ResponseHasValidAccountValues() + { + //Arrange + ApiResponse result; + + var accountId = "test"; + var accountJson = GetAccountJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountJson); + + result = await _testClient.Accounts.GetAccountAsync(accountId); + } + + //Assert + Assert.NotNull(result.Data); + Assert.Equal("8bfc20d7-f7c6-4422-bf07-8243ca4169fe", result.Data.Id); + Assert.Equal("BTC Wallet", result.Data.Name); + Assert.Equal("BTC", result.Data.Currency); + Assert.Equal(1.23m, result.Data.AvailableBalance.Value); + Assert.Equal("BTC", result.Data.AvailableBalance.Currency); + Assert.False(result.Data.Default); + Assert.True(result.Data.Active); + Assert.Equal("2021-05-31T09:59:59", result.Data.CreatedAt.Value.ToString("s", new CultureInfo("en-US"))); + Assert.Equal("2021-05-31T09:59:59", result.Data.UpdatedAt.Value.ToString("s", new CultureInfo("en-US"))); + Assert.Equal("2021-05-31T09:59:59", result.Data.DeletedAt.Value.ToString("s", new CultureInfo("en-US"))); + Assert.Equal("ACCOUNT_TYPE_UNSPECIFIED", result.Data.Type); + Assert.True(result.Data.Ready); + Assert.Equal(1.23m, result.Data.Hold.Value); + Assert.Equal("BTC", result.Data.Hold.Currency); + } + + [Fact] + public async Task GetAccountAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var accountId = "test"; + var invalidJson = GetInvalidAccountJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(invalidJson); + + result = await _testClient.Accounts.GetAccountAsync(accountId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(" ")] + [InlineData("\t")] + [InlineData(null)] + public async Task GetAccountAsync_NullOrWhitespaceAccountId_ReturnsUnsuccessfulApiResponse(string accountId) + { + //Arrange + ApiResponse result; + + var accountJson = GetAccountJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(accountJson); + + result = await _testClient.Accounts.GetAccountAsync(accountId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task GetAccountAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var accountId = "test"; + var accountJson = GetAccountJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Accounts.GetAccountAsync(accountId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetAccountAsync + + #region Test Response Json + + private string GetAccountsListJsonString() + { + var json = + """ + { + "accounts": [ + { + "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", + "name": "BTC Wallet", + "currency": "BTC", + "available_balance": { + "value": "1.23", + "currency": "BTC" + }, + "default": false, + "active": true, + "created_at": "2021-05-31T09:59:59Z", + "updated_at": "2021-05-31T09:59:59Z", + "deleted_at": "2021-05-31T09:59:59Z", + "type": "ACCOUNT_TYPE_UNSPECIFIED", + "ready": true, + "hold": { + "value": "1.23", + "currency": "BTC" + } + }, + { + "uuid": "9bfc20d7-e7c5-3421-af06-9243ca4169ff", + "name": "ETH Wallet", + "currency": "ETH", + "available_balance": { + "value": "1.23", + "currency": "ETH" + }, + "default": false, + "active": true, + "created_at": "2021-05-31T09:59:59Z", + "updated_at": "2021-05-31T09:59:59Z", + "deleted_at": "2021-05-31T09:59:59Z", + "type": "ACCOUNT_TYPE_UNSPECIFIED", + "ready": true, + "hold": { + "value": "1.23", + "currency": "ETH" + } + } + ], + "has_next": true, + "cursor": "789100", + "size": "2" + } + """; + + return json; + } + + + private string GetInvalildAccountsListJsonString() + { + var json = + """ + { + "accounts": [ + { + "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", + "name": "BTC Wallet", + "currency": "BTC", + "available_balance": { + "value": "1.23", + "currency": "BTC" + }, + "default": false, + "active": true, + "created_at": "2021-05-31T09:59:59Z", + "updated_at": "2021-05-31T09:59:59Z", + "deleted_at": "2021-05-31T09:59:59Z", + "type": "ACCOUNT_TYPE_UNSPECIFIED", + "ready": true, + "hold": { + "value": "1.23", + "currency": "BTC" + } + }, + { + "uuid": "9bfc20d7-e7c5-3421-af06-9243ca4169ff", + "name": "ETH Wallet", + "currency": "ETH", + "available_balance": { + "value": "1.23", + "currency": "ETH" + }, + "default": false, + "active": true, + "created_at": "2021-05-31T09:59:59Z", + "updated_at": "2021-05-31T09:59:59Z", + "deleted_at": "2021-05-31T09:59:59Z", + "type": "ACCOUNT_TYPE_UNSPECIFIED", + "ready": true, + "hold": { + "value": "1.23", + "currency": "ETH" + } + } + ], + "has_next": true, + "cursor": "789100", + "size": "invalid" + } + """; + + return json; + } + + private string GetAccountJsonString() + { + var json = + """ + { + "account": { + "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", + "name": "BTC Wallet", + "currency": "BTC", + "available_balance": { + "value": "1.23", + "currency": "BTC" + }, + "default": false, + "active": true, + "created_at": "2021-05-31T09:59:59Z", + "updated_at": "2021-05-31T09:59:59Z", + "deleted_at": "2021-05-31T09:59:59Z", + "type": "ACCOUNT_TYPE_UNSPECIFIED", + "ready": true, + "hold": { + "value": "1.23", + "currency": "BTC" + } + } + } + """; + + return json; + } + + private string GetInvalidAccountJsonString() + { + var json = + """ + { + "account": { + "uuid": "8bfc20d7-f7c6-4422-bf07-8243ca4169fe", + "name": "BTC Wallet", + "currency": "BTC", + "available_balance": { + "value": "1.23", + "currency": "BTC" + }, + "default": false, + "active": "invalid", + "created_at": "2021-05-31T09:59:59Z", + "updated_at": "2021-05-31T09:59:59Z", + "deleted_at": "2021-05-31T09:59:59Z", + "type": "ACCOUNT_TYPE_UNSPECIFIED", + "ready": true, + "hold": { + "value": "1.23", + "currency": "BTC" + } + } + } + """; + + return json; + } + + #endregion // Test Response Json + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/OrdersEndpointTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/OrdersEndpointTests.cs index c20f935..6df03ea 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/OrdersEndpointTests.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/OrdersEndpointTests.cs @@ -1,1675 +1,1671 @@ -using CoinbaseAdvancedTradeClient.Enums; -using CoinbaseAdvancedTradeClient.Interfaces; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Api.Orders; -using CoinbaseAdvancedTradeClient.Models.Config; -using CoinbaseAdvancedTradeClient.Models.Pages; -using FakeItEasy; -using Flurl.Http; -using Flurl.Http.Testing; -using Xunit; - -namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints -{ - public class OrdersEndpointTests - { - private readonly ICoinbaseAdvancedTradeApiClient _testClient; - - public OrdersEndpointTests() - { - var config = new ApiClientConfig() - { - ApiKey = "key", - ApiSecret = "secret" - }; - - _testClient = new CoinbaseAdvancedTradeApiClient(config); - } - - #region GetListOrdersAsync - - [Fact] - public async Task GetListOrdersAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var json = GetOrdersListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListOrdersAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetListOrdersAsync_ValidRequestAndResponseJson_ResultHasValidOrdersPage() - { - //Arrange - ApiResponse result; - - var json = GetOrdersListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListOrdersAsync(); - } - - //Assert - Assert.Null(result.Data.Order); - Assert.NotNull(result.Data.Orders); - } - - [Fact] - public async Task GetListOrdersAsync_ValidRequestAndResponseJson_ResponseHasValidOrders() - { - //Arrange - ApiResponse result; - - var json = GetOrdersListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListOrdersAsync(); - } - - //Assert - Assert.NotNull(result.Data.Orders); - Assert.Contains(result.Data.Orders, o => o.Id.Equals("0000-000000-000000", StringComparison.InvariantCultureIgnoreCase)); - } - - [Fact] - public async Task GetListOrdersAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var invalidJson = GetInvalidOrdersListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(invalidJson); - - result = await _testClient.Orders.GetListOrdersAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(251)] - [InlineData(-1)] - public async Task GetListOrdersAsync_InvalidLimitParameter_ReturnsUnsuccessfulApiResponse(int limit) - { - //Arrange - ApiResponse result; - - var json = GetOrdersListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListOrdersAsync(limit: limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task GetListOrdersAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Orders.GetListOrdersAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetListOrdersAsync - - #region GetListFillsAsync - - [Fact] - public async Task GetListFillsAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var json = GetFillsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListFillsAsync(json); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetListFillsAsync_ValidRequestAndResponseJson_ResultHasValidFillsPage() - { - //Arrange - ApiResponse result; - - var json = GetFillsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListFillsAsync(); - } - - //Assert - Assert.NotNull(result.Data.Fills); - } - - [Fact] - public async Task GetListFillsAsync_ValidRequestAndResponseJson_ResponseHasValidFills() - { - //Arrange - ApiResponse result; - - var json = GetFillsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetListFillsAsync(json); - } - - //Assert - Assert.NotNull(result.Data.Fills); - Assert.Contains(result.Data.Fills, f => f.EntryId.Equals("22222-2222222-22222222", StringComparison.InvariantCultureIgnoreCase)); - } - - [Fact] - public async Task GetListFillsAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var invalidJson = GetInvalidFillsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(invalidJson); - - result = await _testClient.Orders.GetListFillsAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(251)] - [InlineData(-25)] - public async Task GetListFillsAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) - { - //Arrange - ApiResponse result; - - var json = GetFillsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - result = await _testClient.Orders.GetListFillsAsync(limit: limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task GetListFillsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Orders.GetListFillsAsync(); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetListFillsAsync - - #region GetOrderAsync - - [Fact] - public async Task GetOrderAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var orderId = "0000-000000-000000"; - - var json = GetOrderJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetOrderAsync(orderId); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetOrderAsync_ValidRequestAndResponseJson_ResponseHasValidOrder() - { - //Arrange - ApiResponse result; - - var orderId = "0000-000000-000000"; - - var expectedDate = DateTime.Parse("2021-05-31T09:59:59Z", styles: System.Globalization.DateTimeStyles.AdjustToUniversal); - - var json = GetOrderJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.GetOrderAsync(orderId); - } - - //Assert - Assert.NotNull(result.Data); - Assert.Equal("0000-000000-000000", result.Data.Id); - Assert.Equal("BTC-USD", result.Data.ProductId); - Assert.Equal("2222-000000-000000", result.Data.UserId); - Assert.Equal("10.00", result.Data.OrderConfiguration.MarketIoc.QuoteSize); - Assert.Equal("0.001", result.Data.OrderConfiguration.MarketIoc.BaseSize); - Assert.Equal("0.001", result.Data.OrderConfiguration.LimitGtc.BaseSize); - Assert.Equal("10000.00", result.Data.OrderConfiguration.LimitGtc.LimitPrice); - Assert.False(result.Data.OrderConfiguration.LimitGtc.PostOnly); - Assert.Equal("0.001", result.Data.OrderConfiguration.LimitGtd.BaseSize); - Assert.Equal("10000.00", result.Data.OrderConfiguration.LimitGtd.LimitPrice); - Assert.Equal(expectedDate, result.Data.OrderConfiguration.LimitGtd.EndTime); - Assert.False(result.Data.OrderConfiguration.LimitGtd.PostOnly); - Assert.Equal("0.001", result.Data.OrderConfiguration.StopLimitGtc.BaseSize); - Assert.Equal("10000.00", result.Data.OrderConfiguration.StopLimitGtc.LimitPrice); - Assert.Equal("20000.00", result.Data.OrderConfiguration.StopLimitGtc.StopPrice); - Assert.Equal(StopDirection.Up, result.Data.OrderConfiguration.StopLimitGtc.StopDirection); - Assert.Equal("0.001", result.Data.OrderConfiguration.StopLimitGtd.BaseSize); - Assert.Equal("10000.00", result.Data.OrderConfiguration.StopLimitGtd.LimitPrice); - Assert.Equal("20000.00", result.Data.OrderConfiguration.StopLimitGtd.StopPrice); - Assert.Equal(expectedDate, result.Data.OrderConfiguration.StopLimitGtd.EndTime); - Assert.Equal(StopDirection.Up, result.Data.OrderConfiguration.StopLimitGtd.StopDirection); - Assert.Equal(OrderSide.Buy, result.Data.Side); - Assert.Equal("11111-000000-000000", result.Data.ClientOrderId); - Assert.Equal("OPEN", result.Data.Status); - Assert.Equal(TimeInForce.ImmediateOrCancel, result.Data.TimeInForce); - Assert.Equal(expectedDate, result.Data.CreatedTime); - Assert.Equal(50m, result.Data.CompletionPercentage); - Assert.Equal(0.001m, result.Data.FilledSize); - Assert.Equal(50m, result.Data.AverageFilledPrice); - Assert.Equal(1.23m, result.Data.Fee); - Assert.Equal(2m, result.Data.NumberOfFills); - Assert.Equal(10000m, result.Data.FilledValue); - Assert.True(result.Data.PendingCancel); - Assert.False(result.Data.SizeInQuote); - Assert.Equal(5.00m, result.Data.TotalFees); - Assert.False(result.Data.SizeInclusiveOfFees); - Assert.Equal(123.45m, result.Data.TotalValueAfterFees); - Assert.Equal("UNKNOWN_TRIGGER_STATUS", result.Data.TriggerStatus); - Assert.Equal(OrderType.Market, result.Data.OrderType); - Assert.Equal("REJECT_REASON_UNSPECIFIED", result.Data.RejectReason); - Assert.True(result.Data.Settled); - Assert.Equal(ProductType.Spot, result.Data.ProductType); - Assert.Equal("string", result.Data.RejectMessage); - Assert.Equal("string", result.Data.CancelMessage); - Assert.Equal(OrderPlacementSource.RetailAdvanced, result.Data.OrderPlacementSource); - } - - [Fact] - public async Task GetOrderAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var orderId = "0000-000000-000000"; - - var invalidJson = GetInvalidOrderJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(invalidJson); - - result = await _testClient.Orders.GetOrderAsync(orderId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(" ")] - [InlineData("\t")] - [InlineData(null)] - public async Task GetOrderAsync_NullOrWhitespaceOrderId_ReturnsUnsuccessfulApiResponse(string orderId) - { - //Arrange - ApiResponse result; - - var json = GetOrderJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - result = await _testClient.Orders.GetOrderAsync(orderId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task GetOrderAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var orderId = "0000-000000-000000"; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Orders.GetOrderAsync(orderId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetOrderAsync - - #region PostCreateOrderAsync - - [Fact] - public async Task PostCreateOrderAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = "BTC-USD", - Side = OrderSide.Buy, - OrderConfiguration = A.Dummy() - }; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task PostCreateOrderAsync_ValidRequestAndResponseJson_ResponseHasValidCreateOrderSuccessResponse() - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = "BTC-USD", - Side = OrderSide.Buy, - OrderConfiguration = A.Dummy() - }; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.True(result.Success); - Assert.NotNull(result.Data); - Assert.True(result.Data.Success); - Assert.NotNull(result.Data.SuccessResponse); - Assert.Null(result.Data.ErrorResponse); - Assert.Equal("Test123", result.Data.OrderId, true); - Assert.Equal("BTC-USD", result.Data.SuccessResponse.ProductId, true); - Assert.Equal(OrderSide.Buy, result.Data.SuccessResponse.Side); - Assert.Equal("Client123", result.Data.SuccessResponse.ClientOrderId, true); - } - - [Fact] - public async Task PostCreateOrderAsync_ValidRequestAndResponseJson_ResponseHasValidCreateOrderErrorResponse() - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = "BTC-USD", - Side = OrderSide.Buy, - OrderConfiguration = A.Dummy() - }; - - var json = GetValidCreateOrderErrorResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.True(result.Success); - Assert.NotNull(result.Data); - Assert.False(result.Data.Success); - Assert.NotNull(result.Data.ErrorResponse); - Assert.Null(result.Data.SuccessResponse); - Assert.Equal("Test123", result.Data.OrderId, true); - Assert.Equal("Test failure reason.", result.Data.FailureReason, true); - Assert.Equal("Test Error", result.Data.ErrorResponse.Error, true); - Assert.Equal("Test error message.", result.Data.ErrorResponse.Message, true); - Assert.Equal("Test error details.", result.Data.ErrorResponse.ErrorDetails, true); - Assert.Equal("Preview failure reason", result.Data.ErrorResponse.PreviewFailureReason, true); - Assert.Equal("New order failure reason", result.Data.ErrorResponse.NewOrderFailureReason, true); - } - - [Fact] - public async Task PostCreateOrderAsync_NullCreateOrderParameters_ThrowsArgumentNullException() - { - //Arrange - ApiResponse result; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(null); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("\t")] - public async Task PostCreateOrderAsync_NullOrWhiteSpaceProductId_ThrowsArgumentException(string productId) - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = productId, - Side = OrderSide.Buy, - OrderConfiguration = A.Dummy() - }; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task PostCreateOrderAsync_NullOrderConfiguration_ThrowsArgumentException() - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = "BTC-USD", - Side = OrderSide.Buy - }; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task PostCreateOrderAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = "BTC-USD", - Side = OrderSide.Buy, - OrderConfiguration = A.Dummy() - }; - - var json = GetInvalidCreateOrderResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task PostCreateOrderAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var createOrder = new CreateOrderParameters - { - ProductId = "BTC-USD", - Side = OrderSide.Buy, - OrderConfiguration = A.Dummy() - }; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Orders.PostCreateOrderAsync(createOrder); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // PostCreateOrderAsync - - #region PostCancelOrdersAsync - - [Fact] - public async Task PostCancelOrders_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var cancelOrders = new CancelOrdersParameters - { - OrderIds = new List() { "Test123", "Test456" } - }; - - var json = GetValidCancelOrdersResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task PostCancelOrders_ValidRequestAndResponseJson_ResponseHasValidCancelOrdersResponse() - { - //Arrange - ApiResponse result; - - var cancelOrders = new CancelOrdersParameters - { - OrderIds = new List() { "Test123", "Test456" } - }; - - var json = GetValidCancelOrdersResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); - } - - //Assert - Assert.NotNull(result.Data.Results); - Assert.NotEmpty(result.Data.Results); - Assert.Contains(result.Data.Results, x => x.Success == true && x.OrderId.Equals("Test123", StringComparison.InvariantCultureIgnoreCase) && x.FailureReason == null); - Assert.Contains(result.Data.Results, x => x.Success == false && x.OrderId.Equals("Test456", StringComparison.InvariantCultureIgnoreCase) && x.FailureReason.Equals("Test failure reason.", StringComparison.InvariantCultureIgnoreCase)); - } - - [Fact] - public async Task PostCancelOrders_NullCancelOrdersParameters_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var json = GetValidCancelOrdersResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCancelOrdersAsync(null); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task PostCancelOrders_NullOrderIds_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var cancelOrders = new CancelOrdersParameters(); - - var json = GetValidCancelOrdersResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task PostCancelOrders_EmptyOrderIds_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var cancelOrders = new CancelOrdersParameters - { - OrderIds = new List() - }; - - var json = GetValidCancelOrdersResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - - [Fact] - public async Task PostCancelOrders_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var cancelOrders = new CancelOrdersParameters - { - OrderIds = new List() { "Test123", "Test456" } - }; - - var json = GetInvalidCancelOrdersResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task PostCancelOrders_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var cancelOrders = new CancelOrdersParameters - { - OrderIds = new List() { "Test123", "Test456" } - }; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // PostCancelOrdersAsync - - #region CreateMarketOrderAsync - - [Fact] - public async Task CreateMarketOrderAsync_ValidParameters_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var amount = 1.23m; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.CreateMarketOrderAsync(orderSide, productId, amount); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("\t")] - public async Task CreateMarketOrderAsync_InvalidProductId_ThrowsArgumentNullException(string productId) - { - //Arrange - var orderSide = OrderSide.Buy; - var amount = 1.23m; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateMarketOrderAsync(orderSide, productId, amount)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1.23)] - public async Task CreateMarketOrderAsync_InvalidAmount_ThrowsArgumentException(decimal amount) - { - //Arrange - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateMarketOrderAsync(orderSide, productId, amount)); - } - - #endregion // CreateMarketOrderAsync - - #region CreateLimitOrderAsync - - [Fact] - public async Task CreateLimitOrderAsync_ValidParameters_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var amount = 1.23m; - var limitPrice = 123.45m; - var postOnly = false; - var endTime = DateTime.Now; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("\t")] - public async Task CreateLimitOrderAsync_InvalidProductId_ThrowsArgumentNullException(string productId) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var amount = 1.23m; - var limitPrice = 123.45m; - var postOnly = false; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1.23)] - public async Task CreateLimitOrderAsync_InvalidAmount_ThrowsArgumentException(decimal amount) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var limitPrice = 123.45m; - var postOnly = false; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1.23)] - public async Task CreateLimitOrderAsync_InvalidLimitPrice_ThrowsArgumentException(decimal limitPrice) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var amount = 1.23m; - var postOnly = false; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime)); - } - - #endregion // CreateLimitOrderAsync - - #region CreateStopLimitOrderAsync - - [Fact] - public async Task CreateStopLimitOrderAsync_ValidParameters_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var amount = 1.23m; - var limitPrice = 123.45m; - var stopPrice = 234.56m; - var stopDirection = StopDirection.Up; - var endTime = DateTime.Now; - - var json = GetValidCreateOrderSuccessResponse(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("\t")] - public async Task CreateStopLimitOrderAsync_InvalidProductId_ThrowsArgumentNullException(string productId) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var amount = 1.23m; - var limitPrice = 123.45m; - var stopPrice = 234.56m; - var stopDirection = StopDirection.Up; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1.23)] - public async Task CreateStopLimitOrderAsync_InvalidAmount_ThrowsArgumentException(decimal amount) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var limitPrice = 123.45m; - var stopPrice = 234.56m; - var stopDirection = StopDirection.Up; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1.23)] - public async Task CreateStopLimitOrderAsync_InvalidLimitPrice_ThrowsArgumentException(decimal limitPrice) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var amount = 1.23m; - var stopPrice = 234.56m; - var stopDirection = StopDirection.Up; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1.23)] - public async Task CreateStopLimitOrderAsync_InvalidStopPrice_ThrowsArgumentException(decimal stopPrice) - { - //Arrange - var timeInForce = TimeInForce.GoodUntilDate; - var orderSide = OrderSide.Buy; - var productId = "TEST-USD"; - var amount = 1.23m; - var limitPrice = 123.45m; - var stopDirection = StopDirection.Up; - var endTime = DateTime.Now; - - //Act & Assert - await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); - } - - #endregion // CreateStopLimitOrderAsync - - #region Test Response Json - - private string GetOrdersListJsonString() - { - var json = - """ - { - "orders": [ - { - "order_id": "0000-000000-000000", - "product_id": "BTC-USD", - "user_id": "2222-000000-000000", - "order_configuration": { - "market_market_ioc": { - "quote_size": "10.00", - "base_size": "0.001" - }, - "limit_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "post_only": false - }, - "limit_limit_gtd": { - "base_size": "0.001", - "limit_price": "10000.00", - "end_time": "2021-05-31T09:59:59Z", - "post_only": false - }, - "stop_limit_stop_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "stop_price": "20000.00", - "stop_direction": "STOP_DIRECTION_STOP_UP" - }, - "stop_limit_stop_limit_gtd": { - "base_size": 0.001, - "limit_price": "10000.00", - "stop_price": "20000.00", - "end_time": "2021-05-31T09:59:59Z", - "stop_direction": "STOP_DIRECTION_STOP_UP" - } - }, - "side": "BUY", - "client_order_id": "11111-000000-000000", - "status": "OPEN", - "time_in_force": "IMMEDIATE_OR_CANCEL", - "created_time": "2021-05-31T09:59:59Z", - "completion_percentage": "50", - "filled_size": "0.001", - "average_filled_price": "50", - "fee": "123.45", - "number_of_fills": "2", - "filled_value": "10000", - "pending_cancel": true, - "size_in_quote": false, - "total_fees": "5.00", - "size_inclusive_of_fees": false, - "total_value_after_fees": "123.45", - "trigger_status": "UNKNOWN_TRIGGER_STATUS", - "order_type": "MARKET", - "reject_reason": "REJECT_REASON_UNSPECIFIED", - "settled": true, - "product_type": "SPOT", - "reject_message": "string", - "cancel_message": "string", - "order_placement_source": "RETAIL_ADVANCED" - } - ], - "sequence": "12345", - "has_next": true, - "cursor": "789100" - } - """; - - return json; - } - - private string GetInvalidOrdersListJsonString() - { - var json = - """ - { - "orders": [ - { - "order_id": "0000-000000-000000", - "product_id": "BTC-USD", - "user_id": "2222-000000-000000", - "order_configuration": { - "market_market_ioc": { - "quote_size": "10.00", - "base_size": "0.001" - }, - "limit_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "post_only": "INVALID" - }, - "limit_limit_gtd": { - "base_size": "0.001", - "limit_price": "10000.00", - "end_time": "2021-05-31T09:59:59Z", - "post_only": false - }, - "stop_limit_stop_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "stop_price": "20000.00", - "stop_direction": "STOP_DIRECTION_STOP_UP" - }, - "stop_limit_stop_limit_gtd": { - "base_size": 0.001, - "limit_price": "10000.00", - "stop_price": "20000.00", - "end_time": "2021-05-31T09:59:59Z", - "stop_direction": "STOP_DIRECTION_STOP_UP" - } - }, - "side": "BUY", - "client_order_id": "11111-000000-000000", - "status": "OPEN", - "time_in_force": "IMMEDIATE_OR_CANCEL", - "created_time": "2021-05-31T09:59:59Z", - "completion_percentage": "50", - "filled_size": "0.001", - "average_filled_price": "50", - "fee": "123.45", - "number_of_fills": "2", - "filled_value": "10000", - "pending_cancel": true, - "size_in_quote": false, - "total_fees": "5.00", - "size_inclusive_of_fees": false, - "total_value_after_fees": "123.45", - "trigger_status": "UNKNOWN_TRIGGER_STATUS", - "order_type": "MARKET", - "reject_reason": "REJECT_REASON_UNSPECIFIED", - "settled": true, - "product_type": "SPOT", - "reject_message": "string", - "cancel_message": "string", - "order_placement_source": "RETAIL_ADVANCED" - } - ], - "sequence": "12345", - "has_next": true, - "cursor": "789100" - } - """; - - return json; - } - - private string GetFillsListJsonString() - { - var json = - """ - { - "fills": [ - { - "entry_id": "22222-2222222-22222222", - "trade_id": "1111-11111-111111", - "order_id": "0000-000000-000000", - "trade_time": "2021-05-31T09:59:59Z", - "trade_type": "FILL", - "price": "10000.00", - "size": "0.001", - "commission": "1.25", - "product_id": "BTC-USD", - "sequence_timestamp": "2021-05-31T09:58:59Z", - "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", - "size_in_quote": false, - "user_id": "3333-333333-3333333", - "side": "BUY" - } - ], - "cursor": "789100" - } - """; - - return json; - } - - private string GetInvalidFillsListJsonString() - { - var json = - """ - { - "fills": [ - { - "entry_id": "22222-2222222-22222222", - "trade_id": "1111-11111-111111", - "order_id": "0000-000000-000000", - "trade_time": "2021-05-31T09:59:59Z", - "trade_type": "FILL", - "price": "10000.00", - "size": "0.001", - "commission": "1.25", - "product_id": "BTC-USD", - "sequence_timestamp": "2021-05-31T09:58:59Z", - "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", - "size_in_quote": "INVALID", - "user_id": "3333-333333-3333333", - "side": "BUY" - } - ], - "cursor": "789100" - } - """; - - return json; - } - - private string GetOrderJsonString() - { - var json = - """ - { - "order": { - "order_id": "0000-000000-000000", - "product_id": "BTC-USD", - "user_id": "2222-000000-000000", - "order_configuration": { - "market_market_ioc": { - "quote_size": "10.00", - "base_size": "0.001" - }, - "limit_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "post_only": false - }, - "limit_limit_gtd": { - "base_size": "0.001", - "limit_price": "10000.00", - "end_time": "2021-05-31T09:59:59Z", - "post_only": false - }, - "stop_limit_stop_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "stop_price": "20000.00", - "stop_direction": "STOP_DIRECTION_STOP_UP" - }, - "stop_limit_stop_limit_gtd": { - "base_size": 0.001, - "limit_price": "10000.00", - "stop_price": "20000.00", - "end_time": "2021-05-31T09:59:59Z", - "stop_direction": "STOP_DIRECTION_STOP_UP" - } - }, - "side": "BUY", - "client_order_id": "11111-000000-000000", - "status": "OPEN", - "time_in_force": "IMMEDIATE_OR_CANCEL", - "created_time": "2021-05-31T09:59:59Z", - "completion_percentage": "50", - "filled_size": "0.001", - "average_filled_price": "50", - "fee": "1.23", - "number_of_fills": "2", - "filled_value": "10000", - "pending_cancel": true, - "size_in_quote": false, - "total_fees": "5.00", - "size_inclusive_of_fees": false, - "total_value_after_fees": "123.45", - "trigger_status": "UNKNOWN_TRIGGER_STATUS", - "order_type": "MARKET", - "reject_reason": "REJECT_REASON_UNSPECIFIED", - "settled": true, - "product_type": "SPOT", - "reject_message": "string", - "cancel_message": "string", - "order_placement_source": "RETAIL_ADVANCED" - } - } - """; - - return json; - } - - private string GetInvalidOrderJsonString() - { - var json = - """ - { - "order": { - "order_id": "0000-000000-000000", - "product_id": "BTC-USD", - "user_id": "2222-000000-000000", - "order_configuration": { - "market_market_ioc": { - "quote_size": "10.00", - "base_size": "0.001" - }, - "limit_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "post_only": "INVALID" - }, - "limit_limit_gtd": { - "base_size": "0.001", - "limit_price": "10000.00", - "end_time": "2021-05-31T09:59:59Z", - "post_only": false - }, - "stop_limit_stop_limit_gtc": { - "base_size": "0.001", - "limit_price": "10000.00", - "stop_price": "20000.00", - "stop_direction": "STOP_DIRECTION_STOP_UP" - }, - "stop_limit_stop_limit_gtd": { - "base_size": 0.001, - "limit_price": "10000.00", - "stop_price": "20000.00", - "end_time": "2021-05-31T09:59:59Z", - "stop_direction": "STOP_DIRECTION_STOP_UP" - } - }, - "side": "BUY", - "client_order_id": "11111-000000-000000", - "status": "OPEN", - "time_in_force": "IMMEDIATE_OR_CANCEL", - "created_time": "2021-05-31T09:59:59Z", - "completion_percentage": "50", - "filled_size": "0.001", - "average_filled_price": "50", - "fee": "1.23", - "number_of_fills": "2", - "filled_value": "10000", - "pending_cancel": true, - "size_in_quote": false, - "total_fees": "5.00", - "size_inclusive_of_fees": false, - "total_value_after_fees": "123.45", - "trigger_status": "UNKNOWN_TRIGGER_STATUS", - "order_type": "MARKET", - "reject_reason": "REJECT_REASON_UNSPECIFIED", - "settled": "boolean", - "product_type": "SPOT", - "reject_message": "string", - "cancel_message": "string", - "order_placement_source": "RETAIL_ADVANCED" - } - } - """; - - return json; - } - - public string GetValidCreateOrderSuccessResponse() - { - var json = - """ - { - "success": true, - "order_id": "Test123", - "success_response": - { - "order_id": "Test123", - "product_id": "BTC-USD", - "side": "BUY", - "client_order_id": "Client123" - } - - } - """; - - return json; - } - - public string GetValidCreateOrderErrorResponse() - { - var json = - """ - { - "success": false, - "order_id": "Test123", - "failure_reason": "Test failure reason.", - "error_response": - { - "error": "Test Error", - "message": "Test error message.", - "error_details": "Test error details.", - "preview_failure_reason": "Preview failure reason", - "new_order_failure_reason": "New order failure reason" - } - } - """; - - return json; - } - - public string GetInvalidCreateOrderResponse() - { - var json = - """ - { - "success": "INVALID", - "order_id": "Test123", - "success_response": - { - "order_id": "Test123", - "product_id": "BTC-USD", - "side": "BUY", - "client_order_id": "Client123" - } - - } - """; - - return json; - } - - public string GetValidCancelOrdersResponse() - { - var json = - """ - { - "results": [ - { - "success": true, - "order_id": "Test123" - }, - { - "success": false, - "order_id": "Test456", - "failure_reason": "Test failure reason.", - } - ] - } - """; - - return json; - } - - public string GetInvalidCancelOrdersResponse() - { - var json = - """ - { - "results": "INVALID" - } - """; - - return json; - } - - #endregion // Test Response Json - } -} +using CoinbaseAdvancedTradeClient.Enums; +using CoinbaseAdvancedTradeClient.Interfaces; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Api.Orders; +using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.Models.Pages; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; +using FakeItEasy; +using Flurl.Http; +using Flurl.Http.Testing; +using Xunit; + +namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints +{ + public class OrdersEndpointTests + { + private readonly ICoinbaseAdvancedTradeApiClient _testClient; + + public OrdersEndpointTests() + { + var config = TestConfigHelper.CreateTestApiConfig(); + _testClient = new CoinbaseAdvancedTradeApiClient(config); + } + + #region GetListOrdersAsync + + [Fact] + public async Task GetListOrdersAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var json = GetOrdersListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListOrdersAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetListOrdersAsync_ValidRequestAndResponseJson_ResultHasValidOrdersPage() + { + //Arrange + ApiResponse result; + + var json = GetOrdersListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListOrdersAsync(); + } + + //Assert + Assert.Null(result.Data.Order); + Assert.NotNull(result.Data.Orders); + } + + [Fact] + public async Task GetListOrdersAsync_ValidRequestAndResponseJson_ResponseHasValidOrders() + { + //Arrange + ApiResponse result; + + var json = GetOrdersListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListOrdersAsync(); + } + + //Assert + Assert.NotNull(result.Data.Orders); + Assert.Contains(result.Data.Orders, o => o.Id.Equals("0000-000000-000000", StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task GetListOrdersAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var invalidJson = GetInvalidOrdersListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(invalidJson); + + result = await _testClient.Orders.GetListOrdersAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(251)] + [InlineData(-1)] + public async Task GetListOrdersAsync_InvalidLimitParameter_ReturnsUnsuccessfulApiResponse(int limit) + { + //Arrange + ApiResponse result; + + var json = GetOrdersListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListOrdersAsync(limit: limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task GetListOrdersAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Orders.GetListOrdersAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetListOrdersAsync + + #region GetListFillsAsync + + [Fact] + public async Task GetListFillsAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var json = GetFillsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListFillsAsync(json); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetListFillsAsync_ValidRequestAndResponseJson_ResultHasValidFillsPage() + { + //Arrange + ApiResponse result; + + var json = GetFillsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListFillsAsync(); + } + + //Assert + Assert.NotNull(result.Data.Fills); + } + + [Fact] + public async Task GetListFillsAsync_ValidRequestAndResponseJson_ResponseHasValidFills() + { + //Arrange + ApiResponse result; + + var json = GetFillsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetListFillsAsync(json); + } + + //Assert + Assert.NotNull(result.Data.Fills); + Assert.Contains(result.Data.Fills, f => f.EntryId.Equals("22222-2222222-22222222", StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task GetListFillsAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var invalidJson = GetInvalidFillsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(invalidJson); + + result = await _testClient.Orders.GetListFillsAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(251)] + [InlineData(-25)] + public async Task GetListFillsAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) + { + //Arrange + ApiResponse result; + + var json = GetFillsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + result = await _testClient.Orders.GetListFillsAsync(limit: limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task GetListFillsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Orders.GetListFillsAsync(); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetListFillsAsync + + #region GetOrderAsync + + [Fact] + public async Task GetOrderAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var orderId = "0000-000000-000000"; + + var json = GetOrderJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetOrderAsync(orderId); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetOrderAsync_ValidRequestAndResponseJson_ResponseHasValidOrder() + { + //Arrange + ApiResponse result; + + var orderId = "0000-000000-000000"; + + var expectedDate = DateTime.Parse("2021-05-31T09:59:59Z", styles: System.Globalization.DateTimeStyles.AdjustToUniversal); + + var json = GetOrderJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.GetOrderAsync(orderId); + } + + //Assert + Assert.NotNull(result.Data); + Assert.Equal("0000-000000-000000", result.Data.Id); + Assert.Equal("BTC-USD", result.Data.ProductId); + Assert.Equal("2222-000000-000000", result.Data.UserId); + Assert.Equal("10.00", result.Data.OrderConfiguration.MarketIoc.QuoteSize); + Assert.Equal("0.001", result.Data.OrderConfiguration.MarketIoc.BaseSize); + Assert.Equal("0.001", result.Data.OrderConfiguration.LimitGtc.BaseSize); + Assert.Equal("10000.00", result.Data.OrderConfiguration.LimitGtc.LimitPrice); + Assert.False(result.Data.OrderConfiguration.LimitGtc.PostOnly); + Assert.Equal("0.001", result.Data.OrderConfiguration.LimitGtd.BaseSize); + Assert.Equal("10000.00", result.Data.OrderConfiguration.LimitGtd.LimitPrice); + Assert.Equal(expectedDate, result.Data.OrderConfiguration.LimitGtd.EndTime); + Assert.False(result.Data.OrderConfiguration.LimitGtd.PostOnly); + Assert.Equal("0.001", result.Data.OrderConfiguration.StopLimitGtc.BaseSize); + Assert.Equal("10000.00", result.Data.OrderConfiguration.StopLimitGtc.LimitPrice); + Assert.Equal("20000.00", result.Data.OrderConfiguration.StopLimitGtc.StopPrice); + Assert.Equal(StopDirection.Up, result.Data.OrderConfiguration.StopLimitGtc.StopDirection); + Assert.Equal("0.001", result.Data.OrderConfiguration.StopLimitGtd.BaseSize); + Assert.Equal("10000.00", result.Data.OrderConfiguration.StopLimitGtd.LimitPrice); + Assert.Equal("20000.00", result.Data.OrderConfiguration.StopLimitGtd.StopPrice); + Assert.Equal(expectedDate, result.Data.OrderConfiguration.StopLimitGtd.EndTime); + Assert.Equal(StopDirection.Up, result.Data.OrderConfiguration.StopLimitGtd.StopDirection); + Assert.Equal(OrderSide.Buy, result.Data.Side); + Assert.Equal("11111-000000-000000", result.Data.ClientOrderId); + Assert.Equal("OPEN", result.Data.Status); + Assert.Equal(TimeInForce.ImmediateOrCancel, result.Data.TimeInForce); + Assert.Equal(expectedDate, result.Data.CreatedTime); + Assert.Equal(50m, result.Data.CompletionPercentage); + Assert.Equal(0.001m, result.Data.FilledSize); + Assert.Equal(50m, result.Data.AverageFilledPrice); + Assert.Equal(1.23m, result.Data.Fee); + Assert.Equal(2m, result.Data.NumberOfFills); + Assert.Equal(10000m, result.Data.FilledValue); + Assert.True(result.Data.PendingCancel); + Assert.False(result.Data.SizeInQuote); + Assert.Equal(5.00m, result.Data.TotalFees); + Assert.False(result.Data.SizeInclusiveOfFees); + Assert.Equal(123.45m, result.Data.TotalValueAfterFees); + Assert.Equal("UNKNOWN_TRIGGER_STATUS", result.Data.TriggerStatus); + Assert.Equal(OrderType.Market, result.Data.OrderType); + Assert.Equal("REJECT_REASON_UNSPECIFIED", result.Data.RejectReason); + Assert.True(result.Data.Settled); + Assert.Equal(ProductType.Spot, result.Data.ProductType); + Assert.Equal("string", result.Data.RejectMessage); + Assert.Equal("string", result.Data.CancelMessage); + Assert.Equal(OrderPlacementSource.RetailAdvanced, result.Data.OrderPlacementSource); + } + + [Fact] + public async Task GetOrderAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var orderId = "0000-000000-000000"; + + var invalidJson = GetInvalidOrderJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(invalidJson); + + result = await _testClient.Orders.GetOrderAsync(orderId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(" ")] + [InlineData("\t")] + [InlineData(null)] + public async Task GetOrderAsync_NullOrWhitespaceOrderId_ReturnsUnsuccessfulApiResponse(string orderId) + { + //Arrange + ApiResponse result; + + var json = GetOrderJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + result = await _testClient.Orders.GetOrderAsync(orderId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task GetOrderAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var orderId = "0000-000000-000000"; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Orders.GetOrderAsync(orderId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetOrderAsync + + #region PostCreateOrderAsync + + [Fact] + public async Task PostCreateOrderAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = "BTC-USD", + Side = OrderSide.Buy, + OrderConfiguration = A.Dummy() + }; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task PostCreateOrderAsync_ValidRequestAndResponseJson_ResponseHasValidCreateOrderSuccessResponse() + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = "BTC-USD", + Side = OrderSide.Buy, + OrderConfiguration = A.Dummy() + }; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.True(result.Success); + Assert.NotNull(result.Data); + Assert.True(result.Data.Success); + Assert.NotNull(result.Data.SuccessResponse); + Assert.Null(result.Data.ErrorResponse); + Assert.Equal("Test123", result.Data.OrderId, true); + Assert.Equal("BTC-USD", result.Data.SuccessResponse.ProductId, true); + Assert.Equal(OrderSide.Buy, result.Data.SuccessResponse.Side); + Assert.Equal("Client123", result.Data.SuccessResponse.ClientOrderId, true); + } + + [Fact] + public async Task PostCreateOrderAsync_ValidRequestAndResponseJson_ResponseHasValidCreateOrderErrorResponse() + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = "BTC-USD", + Side = OrderSide.Buy, + OrderConfiguration = A.Dummy() + }; + + var json = GetValidCreateOrderErrorResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.True(result.Success); + Assert.NotNull(result.Data); + Assert.False(result.Data.Success); + Assert.NotNull(result.Data.ErrorResponse); + Assert.Null(result.Data.SuccessResponse); + Assert.Equal("Test123", result.Data.OrderId, true); + Assert.Equal("Test failure reason.", result.Data.FailureReason, true); + Assert.Equal("Test Error", result.Data.ErrorResponse.Error, true); + Assert.Equal("Test error message.", result.Data.ErrorResponse.Message, true); + Assert.Equal("Test error details.", result.Data.ErrorResponse.ErrorDetails, true); + Assert.Equal("Preview failure reason", result.Data.ErrorResponse.PreviewFailureReason, true); + Assert.Equal("New order failure reason", result.Data.ErrorResponse.NewOrderFailureReason, true); + } + + [Fact] + public async Task PostCreateOrderAsync_NullCreateOrderParameters_ThrowsArgumentNullException() + { + //Arrange + ApiResponse result; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(null); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("\t")] + public async Task PostCreateOrderAsync_NullOrWhiteSpaceProductId_ThrowsArgumentException(string productId) + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = productId, + Side = OrderSide.Buy, + OrderConfiguration = A.Dummy() + }; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task PostCreateOrderAsync_NullOrderConfiguration_ThrowsArgumentException() + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = "BTC-USD", + Side = OrderSide.Buy + }; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task PostCreateOrderAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = "BTC-USD", + Side = OrderSide.Buy, + OrderConfiguration = A.Dummy() + }; + + var json = GetInvalidCreateOrderResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task PostCreateOrderAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var createOrder = new CreateOrderParameters + { + ProductId = "BTC-USD", + Side = OrderSide.Buy, + OrderConfiguration = A.Dummy() + }; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Orders.PostCreateOrderAsync(createOrder); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // PostCreateOrderAsync + + #region PostCancelOrdersAsync + + [Fact] + public async Task PostCancelOrders_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var cancelOrders = new CancelOrdersParameters + { + OrderIds = new List() { "Test123", "Test456" } + }; + + var json = GetValidCancelOrdersResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task PostCancelOrders_ValidRequestAndResponseJson_ResponseHasValidCancelOrdersResponse() + { + //Arrange + ApiResponse result; + + var cancelOrders = new CancelOrdersParameters + { + OrderIds = new List() { "Test123", "Test456" } + }; + + var json = GetValidCancelOrdersResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); + } + + //Assert + Assert.NotNull(result.Data.Results); + Assert.NotEmpty(result.Data.Results); + Assert.Contains(result.Data.Results, x => x.Success == true && x.OrderId.Equals("Test123", StringComparison.InvariantCultureIgnoreCase) && x.FailureReason == null); + Assert.Contains(result.Data.Results, x => x.Success == false && x.OrderId.Equals("Test456", StringComparison.InvariantCultureIgnoreCase) && x.FailureReason.Equals("Test failure reason.", StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task PostCancelOrders_NullCancelOrdersParameters_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var json = GetValidCancelOrdersResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCancelOrdersAsync(null); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task PostCancelOrders_NullOrderIds_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var cancelOrders = new CancelOrdersParameters(); + + var json = GetValidCancelOrdersResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task PostCancelOrders_EmptyOrderIds_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var cancelOrders = new CancelOrdersParameters + { + OrderIds = new List() + }; + + var json = GetValidCancelOrdersResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + + [Fact] + public async Task PostCancelOrders_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var cancelOrders = new CancelOrdersParameters + { + OrderIds = new List() { "Test123", "Test456" } + }; + + var json = GetInvalidCancelOrdersResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task PostCancelOrders_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var cancelOrders = new CancelOrdersParameters + { + OrderIds = new List() { "Test123", "Test456" } + }; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Orders.PostCancelOrdersAsync(cancelOrders); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // PostCancelOrdersAsync + + #region CreateMarketOrderAsync + + [Fact] + public async Task CreateMarketOrderAsync_ValidParameters_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var amount = 1.23m; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.CreateMarketOrderAsync(orderSide, productId, amount); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("\t")] + public async Task CreateMarketOrderAsync_InvalidProductId_ThrowsArgumentNullException(string productId) + { + //Arrange + var orderSide = OrderSide.Buy; + var amount = 1.23m; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateMarketOrderAsync(orderSide, productId, amount)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public async Task CreateMarketOrderAsync_InvalidAmount_ThrowsArgumentException(decimal amount) + { + //Arrange + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateMarketOrderAsync(orderSide, productId, amount)); + } + + #endregion // CreateMarketOrderAsync + + #region CreateLimitOrderAsync + + [Fact] + public async Task CreateLimitOrderAsync_ValidParameters_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var amount = 1.23m; + var limitPrice = 123.45m; + var postOnly = false; + var endTime = DateTime.Now; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("\t")] + public async Task CreateLimitOrderAsync_InvalidProductId_ThrowsArgumentNullException(string productId) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var amount = 1.23m; + var limitPrice = 123.45m; + var postOnly = false; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public async Task CreateLimitOrderAsync_InvalidAmount_ThrowsArgumentException(decimal amount) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var limitPrice = 123.45m; + var postOnly = false; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public async Task CreateLimitOrderAsync_InvalidLimitPrice_ThrowsArgumentException(decimal limitPrice) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var amount = 1.23m; + var postOnly = false; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, postOnly, endTime)); + } + + #endregion // CreateLimitOrderAsync + + #region CreateStopLimitOrderAsync + + [Fact] + public async Task CreateStopLimitOrderAsync_ValidParameters_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var amount = 1.23m; + var limitPrice = 123.45m; + var stopPrice = 234.56m; + var stopDirection = StopDirection.Up; + var endTime = DateTime.Now; + + var json = GetValidCreateOrderSuccessResponse(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("\t")] + public async Task CreateStopLimitOrderAsync_InvalidProductId_ThrowsArgumentNullException(string productId) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var amount = 1.23m; + var limitPrice = 123.45m; + var stopPrice = 234.56m; + var stopDirection = StopDirection.Up; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public async Task CreateStopLimitOrderAsync_InvalidAmount_ThrowsArgumentException(decimal amount) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var limitPrice = 123.45m; + var stopPrice = 234.56m; + var stopDirection = StopDirection.Up; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public async Task CreateStopLimitOrderAsync_InvalidLimitPrice_ThrowsArgumentException(decimal limitPrice) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var amount = 1.23m; + var stopPrice = 234.56m; + var stopDirection = StopDirection.Up; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public async Task CreateStopLimitOrderAsync_InvalidStopPrice_ThrowsArgumentException(decimal stopPrice) + { + //Arrange + var timeInForce = TimeInForce.GoodUntilDate; + var orderSide = OrderSide.Buy; + var productId = "TEST-USD"; + var amount = 1.23m; + var limitPrice = 123.45m; + var stopDirection = StopDirection.Up; + var endTime = DateTime.Now; + + //Act & Assert + await Assert.ThrowsAsync(async () => await _testClient.Orders.CreateStopLimitOrderAsync(timeInForce, orderSide, productId, amount, limitPrice, stopPrice, stopDirection, endTime)); + } + + #endregion // CreateStopLimitOrderAsync + + #region Test Response Json + + private string GetOrdersListJsonString() + { + var json = + """ + { + "orders": [ + { + "order_id": "0000-000000-000000", + "product_id": "BTC-USD", + "user_id": "2222-000000-000000", + "order_configuration": { + "market_market_ioc": { + "quote_size": "10.00", + "base_size": "0.001" + }, + "limit_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "post_only": false + }, + "limit_limit_gtd": { + "base_size": "0.001", + "limit_price": "10000.00", + "end_time": "2021-05-31T09:59:59Z", + "post_only": false + }, + "stop_limit_stop_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "stop_price": "20000.00", + "stop_direction": "STOP_DIRECTION_STOP_UP" + }, + "stop_limit_stop_limit_gtd": { + "base_size": 0.001, + "limit_price": "10000.00", + "stop_price": "20000.00", + "end_time": "2021-05-31T09:59:59Z", + "stop_direction": "STOP_DIRECTION_STOP_UP" + } + }, + "side": "BUY", + "client_order_id": "11111-000000-000000", + "status": "OPEN", + "time_in_force": "IMMEDIATE_OR_CANCEL", + "created_time": "2021-05-31T09:59:59Z", + "completion_percentage": "50", + "filled_size": "0.001", + "average_filled_price": "50", + "fee": "123.45", + "number_of_fills": "2", + "filled_value": "10000", + "pending_cancel": true, + "size_in_quote": false, + "total_fees": "5.00", + "size_inclusive_of_fees": false, + "total_value_after_fees": "123.45", + "trigger_status": "UNKNOWN_TRIGGER_STATUS", + "order_type": "MARKET", + "reject_reason": "REJECT_REASON_UNSPECIFIED", + "settled": true, + "product_type": "SPOT", + "reject_message": "string", + "cancel_message": "string", + "order_placement_source": "RETAIL_ADVANCED" + } + ], + "sequence": "12345", + "has_next": true, + "cursor": "789100" + } + """; + + return json; + } + + private string GetInvalidOrdersListJsonString() + { + var json = + """ + { + "orders": [ + { + "order_id": "0000-000000-000000", + "product_id": "BTC-USD", + "user_id": "2222-000000-000000", + "order_configuration": { + "market_market_ioc": { + "quote_size": "10.00", + "base_size": "0.001" + }, + "limit_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "post_only": "INVALID" + }, + "limit_limit_gtd": { + "base_size": "0.001", + "limit_price": "10000.00", + "end_time": "2021-05-31T09:59:59Z", + "post_only": false + }, + "stop_limit_stop_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "stop_price": "20000.00", + "stop_direction": "STOP_DIRECTION_STOP_UP" + }, + "stop_limit_stop_limit_gtd": { + "base_size": 0.001, + "limit_price": "10000.00", + "stop_price": "20000.00", + "end_time": "2021-05-31T09:59:59Z", + "stop_direction": "STOP_DIRECTION_STOP_UP" + } + }, + "side": "BUY", + "client_order_id": "11111-000000-000000", + "status": "OPEN", + "time_in_force": "IMMEDIATE_OR_CANCEL", + "created_time": "2021-05-31T09:59:59Z", + "completion_percentage": "50", + "filled_size": "0.001", + "average_filled_price": "50", + "fee": "123.45", + "number_of_fills": "2", + "filled_value": "10000", + "pending_cancel": true, + "size_in_quote": false, + "total_fees": "5.00", + "size_inclusive_of_fees": false, + "total_value_after_fees": "123.45", + "trigger_status": "UNKNOWN_TRIGGER_STATUS", + "order_type": "MARKET", + "reject_reason": "REJECT_REASON_UNSPECIFIED", + "settled": true, + "product_type": "SPOT", + "reject_message": "string", + "cancel_message": "string", + "order_placement_source": "RETAIL_ADVANCED" + } + ], + "sequence": "12345", + "has_next": true, + "cursor": "789100" + } + """; + + return json; + } + + private string GetFillsListJsonString() + { + var json = + """ + { + "fills": [ + { + "entry_id": "22222-2222222-22222222", + "trade_id": "1111-11111-111111", + "order_id": "0000-000000-000000", + "trade_time": "2021-05-31T09:59:59Z", + "trade_type": "FILL", + "price": "10000.00", + "size": "0.001", + "commission": "1.25", + "product_id": "BTC-USD", + "sequence_timestamp": "2021-05-31T09:58:59Z", + "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", + "size_in_quote": false, + "user_id": "3333-333333-3333333", + "side": "BUY" + } + ], + "cursor": "789100" + } + """; + + return json; + } + + private string GetInvalidFillsListJsonString() + { + var json = + """ + { + "fills": [ + { + "entry_id": "22222-2222222-22222222", + "trade_id": "1111-11111-111111", + "order_id": "0000-000000-000000", + "trade_time": "2021-05-31T09:59:59Z", + "trade_type": "FILL", + "price": "10000.00", + "size": "0.001", + "commission": "1.25", + "product_id": "BTC-USD", + "sequence_timestamp": "2021-05-31T09:58:59Z", + "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", + "size_in_quote": "INVALID", + "user_id": "3333-333333-3333333", + "side": "BUY" + } + ], + "cursor": "789100" + } + """; + + return json; + } + + private string GetOrderJsonString() + { + var json = + """ + { + "order": { + "order_id": "0000-000000-000000", + "product_id": "BTC-USD", + "user_id": "2222-000000-000000", + "order_configuration": { + "market_market_ioc": { + "quote_size": "10.00", + "base_size": "0.001" + }, + "limit_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "post_only": false + }, + "limit_limit_gtd": { + "base_size": "0.001", + "limit_price": "10000.00", + "end_time": "2021-05-31T09:59:59Z", + "post_only": false + }, + "stop_limit_stop_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "stop_price": "20000.00", + "stop_direction": "STOP_DIRECTION_STOP_UP" + }, + "stop_limit_stop_limit_gtd": { + "base_size": 0.001, + "limit_price": "10000.00", + "stop_price": "20000.00", + "end_time": "2021-05-31T09:59:59Z", + "stop_direction": "STOP_DIRECTION_STOP_UP" + } + }, + "side": "BUY", + "client_order_id": "11111-000000-000000", + "status": "OPEN", + "time_in_force": "IMMEDIATE_OR_CANCEL", + "created_time": "2021-05-31T09:59:59Z", + "completion_percentage": "50", + "filled_size": "0.001", + "average_filled_price": "50", + "fee": "1.23", + "number_of_fills": "2", + "filled_value": "10000", + "pending_cancel": true, + "size_in_quote": false, + "total_fees": "5.00", + "size_inclusive_of_fees": false, + "total_value_after_fees": "123.45", + "trigger_status": "UNKNOWN_TRIGGER_STATUS", + "order_type": "MARKET", + "reject_reason": "REJECT_REASON_UNSPECIFIED", + "settled": true, + "product_type": "SPOT", + "reject_message": "string", + "cancel_message": "string", + "order_placement_source": "RETAIL_ADVANCED" + } + } + """; + + return json; + } + + private string GetInvalidOrderJsonString() + { + var json = + """ + { + "order": { + "order_id": "0000-000000-000000", + "product_id": "BTC-USD", + "user_id": "2222-000000-000000", + "order_configuration": { + "market_market_ioc": { + "quote_size": "10.00", + "base_size": "0.001" + }, + "limit_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "post_only": "INVALID" + }, + "limit_limit_gtd": { + "base_size": "0.001", + "limit_price": "10000.00", + "end_time": "2021-05-31T09:59:59Z", + "post_only": false + }, + "stop_limit_stop_limit_gtc": { + "base_size": "0.001", + "limit_price": "10000.00", + "stop_price": "20000.00", + "stop_direction": "STOP_DIRECTION_STOP_UP" + }, + "stop_limit_stop_limit_gtd": { + "base_size": 0.001, + "limit_price": "10000.00", + "stop_price": "20000.00", + "end_time": "2021-05-31T09:59:59Z", + "stop_direction": "STOP_DIRECTION_STOP_UP" + } + }, + "side": "BUY", + "client_order_id": "11111-000000-000000", + "status": "OPEN", + "time_in_force": "IMMEDIATE_OR_CANCEL", + "created_time": "2021-05-31T09:59:59Z", + "completion_percentage": "50", + "filled_size": "0.001", + "average_filled_price": "50", + "fee": "1.23", + "number_of_fills": "2", + "filled_value": "10000", + "pending_cancel": true, + "size_in_quote": false, + "total_fees": "5.00", + "size_inclusive_of_fees": false, + "total_value_after_fees": "123.45", + "trigger_status": "UNKNOWN_TRIGGER_STATUS", + "order_type": "MARKET", + "reject_reason": "REJECT_REASON_UNSPECIFIED", + "settled": "boolean", + "product_type": "SPOT", + "reject_message": "string", + "cancel_message": "string", + "order_placement_source": "RETAIL_ADVANCED" + } + } + """; + + return json; + } + + public string GetValidCreateOrderSuccessResponse() + { + var json = + """ + { + "success": true, + "order_id": "Test123", + "success_response": + { + "order_id": "Test123", + "product_id": "BTC-USD", + "side": "BUY", + "client_order_id": "Client123" + } + + } + """; + + return json; + } + + public string GetValidCreateOrderErrorResponse() + { + var json = + """ + { + "success": false, + "order_id": "Test123", + "failure_reason": "Test failure reason.", + "error_response": + { + "error": "Test Error", + "message": "Test error message.", + "error_details": "Test error details.", + "preview_failure_reason": "Preview failure reason", + "new_order_failure_reason": "New order failure reason" + } + } + """; + + return json; + } + + public string GetInvalidCreateOrderResponse() + { + var json = + """ + { + "success": "INVALID", + "order_id": "Test123", + "success_response": + { + "order_id": "Test123", + "product_id": "BTC-USD", + "side": "BUY", + "client_order_id": "Client123" + } + + } + """; + + return json; + } + + public string GetValidCancelOrdersResponse() + { + var json = + """ + { + "results": [ + { + "success": true, + "order_id": "Test123" + }, + { + "success": false, + "order_id": "Test456", + "failure_reason": "Test failure reason.", + } + ] + } + """; + + return json; + } + + public string GetInvalidCancelOrdersResponse() + { + var json = + """ + { + "results": "INVALID" + } + """; + + return json; + } + + #endregion // Test Response Json + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/ProductsEndpointTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/ProductsEndpointTests.cs index 89ce47e..e5d8ce5 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/ProductsEndpointTests.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/ProductsEndpointTests.cs @@ -1,1072 +1,1068 @@ -using CoinbaseAdvancedTradeClient.Enums; -using CoinbaseAdvancedTradeClient.Interfaces; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Api.Products; -using CoinbaseAdvancedTradeClient.Models.Config; -using CoinbaseAdvancedTradeClient.Models.Pages; -using CoinbaseAdvancedTradeClient.Resources; -using Flurl.Http; -using Flurl.Http.Testing; -using Xunit; - -namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints -{ - public class ProductsEndpointTests - { - private readonly ICoinbaseAdvancedTradeApiClient _testClient; - - public ProductsEndpointTests() - { - var config = new ApiClientConfig() - { - ApiKey = "key", - ApiSecret = "secret" - }; - - _testClient = new CoinbaseAdvancedTradeApiClient(config); - } - - #region GetListProductsAsync - - [Fact] - public async Task GetListProductsAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var limit = 1; - var offset = 0; - var productType = ProductType.Spot; - - var json = GetProductsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetListProductsAsync_ValidRequestAndResponseJson_ResponseHasValidProductsPage() - { - //Arrange - ApiResponse result; - - var limit = 1; - var offset = 0; - var productType = ProductType.Spot; - - var json = GetProductsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result.Data.Products); - Assert.Equal(100, result.Data.NumberOfProducts); - } - - [Fact] - public async Task GetListProductsAsync_ValidRequestAndResponseJson_ResponseHasValidProducts() - { - //Arrange - ApiResponse result; - - var limit = 1; - var offset = 0; - var productType = ProductType.Spot; - - var json = GetProductsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result.Data.Products); - Assert.True(result.Data.Products.Count > 0); - Assert.Contains(result.Data.Products, p => p.ProductId.Equals("BTC-USD", StringComparison.InvariantCultureIgnoreCase)); - } - - [Fact] - public async Task GetListProductsAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var limit = 1; - var offset = 0; - var productType = ProductType.Spot; - - var json = GetInvalidProductsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(251)] - [InlineData(0)] - [InlineData(-25)] - public async Task GetListProductsAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) - { - //Arrange - ApiResponse result; - - var offset = 0; - var productType = ProductType.Spot; - - var json = GetProductsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - Assert.Contains(ErrorMessages.LimitParameterRange, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task GetListProductsAsync_InvalidOffsetRange_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var limit = 1; - var offset = -1; - var productType = ProductType.Spot; - - var json = GetProductsListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - Assert.Contains(ErrorMessages.OffsetParameterRange, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task GetListProductsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var limit = 1; - var offset = 0; - var productType = ProductType.Spot; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetListProductsAsync - - #region GetProductAsync - - [Fact] - public async Task GetProductAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - - var json = GetProductJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductAsync(productId); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetProductAsync_ValidRequestAndResponseJson_ResponseHasValidProduct() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - - var json = GetProductJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductAsync(productId); - } - - //Assert - Assert.Equal("BTC-USD", result.Data.ProductId); - Assert.Equal(140.21m, result.Data.Price); - Assert.Equal(9.43m, result.Data.PricePercentageChange24H); - Assert.Equal(1908432m, result.Data.Volume24H); - Assert.Equal(9.43m, result.Data.VolumePercentageChange24H); - Assert.Equal(0.00000001m, result.Data.BaseIncrement); - Assert.Equal(0.00000001m, result.Data.QuoteIncrement); - Assert.Equal(0.00000001m, result.Data.QuoteMinSize); - Assert.Equal(1000m, result.Data.QuoteMaxSize); - Assert.Equal(0.00000001m, result.Data.BaseMinSize); - Assert.Equal(1000m, result.Data.BaseMaxSize); - Assert.Equal("Bitcoin", result.Data.BaseName); - Assert.Equal("US Dollar", result.Data.QuoteName); - Assert.True(result.Data.Watched); - Assert.False(result.Data.IsDisabled); - Assert.True(result.Data.IsNew); - Assert.Equal("string", result.Data.Status); - Assert.True(result.Data.CancelOnly); - Assert.True(result.Data.LimitOnly); - Assert.True(result.Data.PostOnly); - Assert.False(result.Data.TradingDisabled); - Assert.True(result.Data.AuctionMode); - Assert.Equal(ProductType.Spot, result.Data.ProductType); - Assert.Equal("USD", result.Data.QuoteCurrencyId); - Assert.Equal("BTC", result.Data.BaseCurrencyId); - Assert.Equal(140.22m, result.Data.MidMarketPrice); - Assert.Equal("BTC", result.Data.BaseDisplaySymbol); - Assert.Equal("USD", result.Data.QuoteDisplaySymbol); - } - - [Fact] - public async Task GetProductAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - - var json = GetInvalidProductJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductAsync(productId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(" ")] - [InlineData("\t")] - [InlineData(null)] - public async Task GetProductAsync_NullOrWhitespaceProductId_ReturnsUnsuccessfulApiResponse(string productId) - { - //Arrange - ApiResponse result; - - var json = GetProductJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductAsync(productId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Fact] - public async Task GetProductsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - - var json = GetProductJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Products.GetProductAsync(productId); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetProductAsync - - #region GetProductCandlesAsync - - [Fact] - public async Task GetProductCandlesAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - var json = GetCandlesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetProductCandlesAsync_ValidRequestAndResponseJson_ResponseHasValidCandlesPage() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - var json = GetCandlesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result.Data.Candles); - Assert.NotEmpty(result.Data.Candles); - } - - [Fact] - public async Task GetProductCandlesAsync_ValidRequestAndResponseJson_ResponseHasValidCandles() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - var json = GetCandlesListJsonString(); - - var expectedDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(1639508050); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result.Data.Candles); - Assert.Contains(result.Data.Candles, c => c.Start.Equals(expectedDate)); - } - - [Fact] - public async Task GetProductCandlesAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - var json = GetInvalidCandlesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(" ")] - [InlineData("\t")] - [InlineData(null)] - public async Task GetProductCandlesAsync_InvalidProductId_ReturnsUnsuccessfulApiResponse(string productId) - { - //Arrange - ApiResponse result; - - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - var json = GetCandlesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - Assert.Contains(ErrorMessages.ProductIdRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task GetProductCandlesAsync_InvalidStartDate_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.MinValue; - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - var json = GetCandlesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - Assert.Contains(ErrorMessages.StartDateRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task GetProductCandlesAsync_InvalidEndDate_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.MinValue; - var granularity = CandleGranularity.OneMinute; - - var json = GetCandlesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - Assert.Contains(ErrorMessages.EndDateRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task GetProductCandlesAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var start = DateTimeOffset.UtcNow.AddDays(-2); - var end = DateTimeOffset.UtcNow.AddDays(-1); - var granularity = CandleGranularity.OneMinute; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetProductCandlesAsync - - #region GetMarketTradesAsync - - [Fact] - public async Task GetMarketTradesAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var limit = 1; - - var json = GetMarketTradesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetMarketTradesAsync(productId, limit); - } - - //Assert - Assert.NotNull(result); - Assert.NotNull(result.Data); - Assert.True(result.Success); - Assert.Null(result.ExceptionType); - Assert.Null(result.ExceptionMessage); - Assert.Null(result.ExceptionDetails); - } - - [Fact] - public async Task GetMarketTradesAsync_ValidRequestAndResponseJson_ResponseHasValidTradesPage() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var limit = 1; - - var json = GetMarketTradesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetMarketTradesAsync(productId, limit); - } - - //Assert - Assert.NotNull(result.Data.Trades); - Assert.NotEmpty(result.Data.Trades); - Assert.Contains(result.Data.Trades, t => t.TradeId.Equals("34b080bf-fcfd-445a-832b-46b5ddc65601", StringComparison.InvariantCultureIgnoreCase)); - Assert.Equal("291.13", result.Data.BestBid); - Assert.Equal("292.40", result.Data.BestAsk); - } - - [Fact] - public async Task GetMarketTradesAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var limit = 1; - - var json = GetInvalidMarketTradesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetMarketTradesAsync(productId, limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.NotNull(result.ExceptionType); - Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(251)] - [InlineData(0)] - [InlineData(-25)] - public async Task GetMarketTradesAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - - var json = GetMarketTradesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetMarketTradesAsync(productId, limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - [Theory] - [InlineData(" ")] - [InlineData("\t")] - [InlineData(null)] - public async Task GetMarketTradesAsync_InvalidProductId_ReturnsUnsuccessfulApiResponse(string productId) - { - //Arrange - ApiResponse result; - - var limit = 1; - - var json = GetMarketTradesListJsonString(); - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(json); - - result = await _testClient.Products.GetMarketTradesAsync(productId, limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - Assert.Contains(ErrorMessages.ProductIdRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task GetMarketTradesAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() - { - //Arrange - ApiResponse result; - - var productId = "TEST"; - var limit = 1; - - //Act - using (var httpTest = new HttpTest()) - { - httpTest.RespondWith(status: 401); - - result = await _testClient.Products.GetMarketTradesAsync(productId, limit); - } - - //Assert - Assert.NotNull(result); - Assert.Null(result.Data); - Assert.False(result.Success); - Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); - Assert.NotNull(result.ExceptionMessage); - Assert.NotNull(result.ExceptionDetails); - } - - #endregion // GetMarketTradesAsync - - #region Test Response Json - - private string GetProductsListJsonString() - { - var json = - """ - { - "products": [ - { - "product_id": "BTC-USD", - "price": "140.21", - "price_percentage_change_24h": "9.43", - "volume_24h": "1908432", - "volume_percentage_change_24h": "9.43", - "base_increment": "0.00000001", - "quote_increment": "0.00000001", - "quote_min_size": "0.00000001", - "quote_max_size": "1000", - "base_min_size": "0.00000001", - "base_max_size": "1000", - "base_name": "Bitcoin", - "quote_name": "US Dollar", - "watched": true, - "is_disabled": false, - "new": true, - "status": "string", - "cancel_only": true, - "limit_only": true, - "post_only": true, - "trading_disabled": false, - "auction_mode": true, - "product_type": "SPOT", - "quote_currency_id": "USD", - "base_currency_id": "BTC", - "mid_market_price": "140.22", - "base_display_symbol": "BTC", - "quote_display_symbol": "USD" - } - ], - "num_products": 100 - } - """; - - return json; - } - - private string GetInvalidProductsListJsonString() - { - var json = - """ - { - "products": [ - { - "product_id": "BTC-USD", - "price": "140.21", - "price_percentage_change_24h": "9.43", - "volume_24h": "1908432", - "volume_percentage_change_24h": "9.43", - "base_increment": "0.00000001", - "quote_increment": "0.00000001", - "quote_min_size": "0.00000001", - "quote_max_size": "1000", - "base_min_size": "0.00000001", - "base_max_size": "1000", - "base_name": "Bitcoin", - "quote_name": "US Dollar", - "watched": "INVALID", - "is_disabled": false, - "new": true, - "status": "string", - "cancel_only": true, - "limit_only": true, - "post_only": true, - "trading_disabled": false, - "auction_mode": true, - "product_type": "SPOT", - "quote_currency_id": "USD", - "base_currency_id": "BTC", - "mid_market_price": "140.22", - "base_display_symbol": "BTC", - "quote_display_symbol": "USD" - } - ], - "num_products": 100 - } - """; - - return json; - } - - private string GetProductJsonString() - { - var json = - """ - { - "product_id": "BTC-USD", - "price": "140.21", - "price_percentage_change_24h": "9.43", - "volume_24h": "1908432", - "volume_percentage_change_24h": "9.43", - "base_increment": "0.00000001", - "quote_increment": "0.00000001", - "quote_min_size": "0.00000001", - "quote_max_size": "1000", - "base_min_size": "0.00000001", - "base_max_size": "1000", - "base_name": "Bitcoin", - "quote_name": "US Dollar", - "watched": true, - "is_disabled": false, - "new": true, - "status": "string", - "cancel_only": true, - "limit_only": true, - "post_only": true, - "trading_disabled": false, - "auction_mode": true, - "product_type": "SPOT", - "quote_currency_id": "USD", - "base_currency_id": "BTC", - "mid_market_price": "140.22", - "base_display_symbol": "BTC", - "quote_display_symbol": "USD" - } - """; - - return json; - } - - private string GetInvalidProductJsonString() - { - var json = - """ - { - "product_id": "BTC-USD", - "price": "140.21", - "price_percentage_change_24h": "9.43", - "volume_24h": "1908432", - "volume_percentage_change_24h": "9.43", - "base_increment": "0.00000001", - "quote_increment": "0.00000001", - "quote_min_size": "0.00000001", - "quote_max_size": "1000", - "base_min_size": "0.00000001", - "base_max_size": "1000", - "base_name": "Bitcoin", - "quote_name": "US Dollar", - "watched": "INVALID", - "is_disabled": false, - "new": true, - "status": "string", - "cancel_only": true, - "limit_only": true, - "post_only": true, - "trading_disabled": false, - "auction_mode": true, - "product_type": "SPOT", - "quote_currency_id": "USD", - "base_currency_id": "BTC", - "mid_market_price": "140.22", - "base_display_symbol": "BTC", - "quote_display_symbol": "USD" - } - """; - - return json; - } - - private string GetCandlesListJsonString() - { - var json = - """ - { - "candles": [ - { - "start": "1639508050", - "low": "140.21", - "high": "140.21", - "open": "140.21", - "close": "140.21", - "volume": "56437345" - } - ] - } - """; - - return json; - } - - private string GetInvalidCandlesListJsonString() - { - var json = - """ - { - "candles": - { - "start": "1639508050", - "low": "140.21", - "high": "140.21", - "open": "140.21", - "close": "140.21", - "volume": "56437345" - } - } - """; - - return json; - } - - private string GetMarketTradesListJsonString() - { - var json = - """ - { - "trades": [ - { - "trade_id": "34b080bf-fcfd-445a-832b-46b5ddc65601", - "product_id": "BTC-USD", - "price": "140.91", - "size": "4", - "time": "2021-05-31T09:59:59Z", - "side": "BUY", - "bid": "291.13", - "ask": "292.40" - } - ], - "best_bid": "291.13", - "best_ask": "292.40" - } - """; - - return json; - } - - private string GetInvalidMarketTradesListJsonString() - { - var json = - """ - { - "trades": - { - "trade_id": "34b080bf-fcfd-445a-832b-46b5ddc65601", - "product_id": "BTC-USD", - "price": "INVALID", - "size": "4", - "time": "2021-05-31T09:59:59Z", - "side": "SELL", - "bid": "291.13", - "ask": "292.40" - }, - "best_bid": "291.13", - "best_ask": "292.40" - } - """; - - return json; - } - - #endregion // Test Response Json - } -} +using CoinbaseAdvancedTradeClient.Enums; +using CoinbaseAdvancedTradeClient.Interfaces; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Api.Products; +using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.Models.Pages; +using CoinbaseAdvancedTradeClient.Resources; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; +using Flurl.Http; +using Flurl.Http.Testing; +using Xunit; + +namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints +{ + public class ProductsEndpointTests + { + private readonly ICoinbaseAdvancedTradeApiClient _testClient; + + public ProductsEndpointTests() + { + var config = TestConfigHelper.CreateTestApiConfig(); + _testClient = new CoinbaseAdvancedTradeApiClient(config); + } + + #region GetListProductsAsync + + [Fact] + public async Task GetListProductsAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var limit = 1; + var offset = 0; + var productType = ProductType.Spot; + + var json = GetProductsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetListProductsAsync_ValidRequestAndResponseJson_ResponseHasValidProductsPage() + { + //Arrange + ApiResponse result; + + var limit = 1; + var offset = 0; + var productType = ProductType.Spot; + + var json = GetProductsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result.Data.Products); + Assert.Equal(100, result.Data.NumberOfProducts); + } + + [Fact] + public async Task GetListProductsAsync_ValidRequestAndResponseJson_ResponseHasValidProducts() + { + //Arrange + ApiResponse result; + + var limit = 1; + var offset = 0; + var productType = ProductType.Spot; + + var json = GetProductsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result.Data.Products); + Assert.True(result.Data.Products.Count > 0); + Assert.Contains(result.Data.Products, p => p.ProductId.Equals("BTC-USD", StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task GetListProductsAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var limit = 1; + var offset = 0; + var productType = ProductType.Spot; + + var json = GetInvalidProductsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(251)] + [InlineData(0)] + [InlineData(-25)] + public async Task GetListProductsAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) + { + //Arrange + ApiResponse result; + + var offset = 0; + var productType = ProductType.Spot; + + var json = GetProductsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + Assert.Contains(ErrorMessages.LimitParameterRange, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task GetListProductsAsync_InvalidOffsetRange_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var limit = 1; + var offset = -1; + var productType = ProductType.Spot; + + var json = GetProductsListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + Assert.Contains(ErrorMessages.OffsetParameterRange, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task GetListProductsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var limit = 1; + var offset = 0; + var productType = ProductType.Spot; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Products.GetListProductsAsync(limit, offset, productType); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetListProductsAsync + + #region GetProductAsync + + [Fact] + public async Task GetProductAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + + var json = GetProductJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductAsync(productId); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetProductAsync_ValidRequestAndResponseJson_ResponseHasValidProduct() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + + var json = GetProductJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductAsync(productId); + } + + //Assert + Assert.Equal("BTC-USD", result.Data.ProductId); + Assert.Equal(140.21m, result.Data.Price); + Assert.Equal(9.43m, result.Data.PricePercentageChange24H); + Assert.Equal(1908432m, result.Data.Volume24H); + Assert.Equal(9.43m, result.Data.VolumePercentageChange24H); + Assert.Equal(0.00000001m, result.Data.BaseIncrement); + Assert.Equal(0.00000001m, result.Data.QuoteIncrement); + Assert.Equal(0.00000001m, result.Data.QuoteMinSize); + Assert.Equal(1000m, result.Data.QuoteMaxSize); + Assert.Equal(0.00000001m, result.Data.BaseMinSize); + Assert.Equal(1000m, result.Data.BaseMaxSize); + Assert.Equal("Bitcoin", result.Data.BaseName); + Assert.Equal("US Dollar", result.Data.QuoteName); + Assert.True(result.Data.Watched); + Assert.False(result.Data.IsDisabled); + Assert.True(result.Data.IsNew); + Assert.Equal("string", result.Data.Status); + Assert.True(result.Data.CancelOnly); + Assert.True(result.Data.LimitOnly); + Assert.True(result.Data.PostOnly); + Assert.False(result.Data.TradingDisabled); + Assert.True(result.Data.AuctionMode); + Assert.Equal(ProductType.Spot, result.Data.ProductType); + Assert.Equal("USD", result.Data.QuoteCurrencyId); + Assert.Equal("BTC", result.Data.BaseCurrencyId); + Assert.Equal(140.22m, result.Data.MidMarketPrice); + Assert.Equal("BTC", result.Data.BaseDisplaySymbol); + Assert.Equal("USD", result.Data.QuoteDisplaySymbol); + } + + [Fact] + public async Task GetProductAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + + var json = GetInvalidProductJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductAsync(productId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(" ")] + [InlineData("\t")] + [InlineData(null)] + public async Task GetProductAsync_NullOrWhitespaceProductId_ReturnsUnsuccessfulApiResponse(string productId) + { + //Arrange + ApiResponse result; + + var json = GetProductJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductAsync(productId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Fact] + public async Task GetProductsAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + + var json = GetProductJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Products.GetProductAsync(productId); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetProductAsync + + #region GetProductCandlesAsync + + [Fact] + public async Task GetProductCandlesAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + var json = GetCandlesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetProductCandlesAsync_ValidRequestAndResponseJson_ResponseHasValidCandlesPage() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + var json = GetCandlesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result.Data.Candles); + Assert.NotEmpty(result.Data.Candles); + } + + [Fact] + public async Task GetProductCandlesAsync_ValidRequestAndResponseJson_ResponseHasValidCandles() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + var json = GetCandlesListJsonString(); + + var expectedDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(1639508050); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result.Data.Candles); + Assert.Contains(result.Data.Candles, c => c.Start.Equals(expectedDate)); + } + + [Fact] + public async Task GetProductCandlesAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + var json = GetInvalidCandlesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(" ")] + [InlineData("\t")] + [InlineData(null)] + public async Task GetProductCandlesAsync_InvalidProductId_ReturnsUnsuccessfulApiResponse(string productId) + { + //Arrange + ApiResponse result; + + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + var json = GetCandlesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + Assert.Contains(ErrorMessages.ProductIdRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task GetProductCandlesAsync_InvalidStartDate_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.MinValue; + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + var json = GetCandlesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + Assert.Contains(ErrorMessages.StartDateRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task GetProductCandlesAsync_InvalidEndDate_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.MinValue; + var granularity = CandleGranularity.OneMinute; + + var json = GetCandlesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + Assert.Contains(ErrorMessages.EndDateRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task GetProductCandlesAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var start = DateTimeOffset.UtcNow.AddDays(-2); + var end = DateTimeOffset.UtcNow.AddDays(-1); + var granularity = CandleGranularity.OneMinute; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Products.GetProductCandlesAsync(productId, start, end, granularity); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetProductCandlesAsync + + #region GetMarketTradesAsync + + [Fact] + public async Task GetMarketTradesAsync_ValidRequestAndResponseJson_ReturnsSuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var limit = 1; + + var json = GetMarketTradesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetMarketTradesAsync(productId, limit); + } + + //Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Success); + Assert.Null(result.ExceptionType); + Assert.Null(result.ExceptionMessage); + Assert.Null(result.ExceptionDetails); + } + + [Fact] + public async Task GetMarketTradesAsync_ValidRequestAndResponseJson_ResponseHasValidTradesPage() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var limit = 1; + + var json = GetMarketTradesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetMarketTradesAsync(productId, limit); + } + + //Assert + Assert.NotNull(result.Data.Trades); + Assert.NotEmpty(result.Data.Trades); + Assert.Contains(result.Data.Trades, t => t.TradeId.Equals("34b080bf-fcfd-445a-832b-46b5ddc65601", StringComparison.InvariantCultureIgnoreCase)); + Assert.Equal("291.13", result.Data.BestBid); + Assert.Equal("292.40", result.Data.BestAsk); + } + + [Fact] + public async Task GetMarketTradesAsync_InvalidResponseJson_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var limit = 1; + + var json = GetInvalidMarketTradesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetMarketTradesAsync(productId, limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.NotNull(result.ExceptionType); + Assert.Equal(nameof(FlurlParsingException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(251)] + [InlineData(0)] + [InlineData(-25)] + public async Task GetMarketTradesAsync_InvalidLimitRange_ReturnsUnsuccessfulApiResponse(int limit) + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + + var json = GetMarketTradesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetMarketTradesAsync(productId, limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + [Theory] + [InlineData(" ")] + [InlineData("\t")] + [InlineData(null)] + public async Task GetMarketTradesAsync_InvalidProductId_ReturnsUnsuccessfulApiResponse(string productId) + { + //Arrange + ApiResponse result; + + var limit = 1; + + var json = GetMarketTradesListJsonString(); + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(json); + + result = await _testClient.Products.GetMarketTradesAsync(productId, limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(ArgumentNullException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + Assert.Contains(ErrorMessages.ProductIdRequired, result.ExceptionMessage, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task GetMarketTradesAsync_UnauthorizedResponseStatus_ReturnsUnsuccessfulApiResponse() + { + //Arrange + ApiResponse result; + + var productId = "TEST"; + var limit = 1; + + //Act + using (var httpTest = new HttpTest()) + { + httpTest.RespondWith(status: 401); + + result = await _testClient.Products.GetMarketTradesAsync(productId, limit); + } + + //Assert + Assert.NotNull(result); + Assert.Null(result.Data); + Assert.False(result.Success); + Assert.Equal(nameof(FlurlHttpException), result.ExceptionType); + Assert.NotNull(result.ExceptionMessage); + Assert.NotNull(result.ExceptionDetails); + } + + #endregion // GetMarketTradesAsync + + #region Test Response Json + + private string GetProductsListJsonString() + { + var json = + """ + { + "products": [ + { + "product_id": "BTC-USD", + "price": "140.21", + "price_percentage_change_24h": "9.43", + "volume_24h": "1908432", + "volume_percentage_change_24h": "9.43", + "base_increment": "0.00000001", + "quote_increment": "0.00000001", + "quote_min_size": "0.00000001", + "quote_max_size": "1000", + "base_min_size": "0.00000001", + "base_max_size": "1000", + "base_name": "Bitcoin", + "quote_name": "US Dollar", + "watched": true, + "is_disabled": false, + "new": true, + "status": "string", + "cancel_only": true, + "limit_only": true, + "post_only": true, + "trading_disabled": false, + "auction_mode": true, + "product_type": "SPOT", + "quote_currency_id": "USD", + "base_currency_id": "BTC", + "mid_market_price": "140.22", + "base_display_symbol": "BTC", + "quote_display_symbol": "USD" + } + ], + "num_products": 100 + } + """; + + return json; + } + + private string GetInvalidProductsListJsonString() + { + var json = + """ + { + "products": [ + { + "product_id": "BTC-USD", + "price": "140.21", + "price_percentage_change_24h": "9.43", + "volume_24h": "1908432", + "volume_percentage_change_24h": "9.43", + "base_increment": "0.00000001", + "quote_increment": "0.00000001", + "quote_min_size": "0.00000001", + "quote_max_size": "1000", + "base_min_size": "0.00000001", + "base_max_size": "1000", + "base_name": "Bitcoin", + "quote_name": "US Dollar", + "watched": "INVALID", + "is_disabled": false, + "new": true, + "status": "string", + "cancel_only": true, + "limit_only": true, + "post_only": true, + "trading_disabled": false, + "auction_mode": true, + "product_type": "SPOT", + "quote_currency_id": "USD", + "base_currency_id": "BTC", + "mid_market_price": "140.22", + "base_display_symbol": "BTC", + "quote_display_symbol": "USD" + } + ], + "num_products": 100 + } + """; + + return json; + } + + private string GetProductJsonString() + { + var json = + """ + { + "product_id": "BTC-USD", + "price": "140.21", + "price_percentage_change_24h": "9.43", + "volume_24h": "1908432", + "volume_percentage_change_24h": "9.43", + "base_increment": "0.00000001", + "quote_increment": "0.00000001", + "quote_min_size": "0.00000001", + "quote_max_size": "1000", + "base_min_size": "0.00000001", + "base_max_size": "1000", + "base_name": "Bitcoin", + "quote_name": "US Dollar", + "watched": true, + "is_disabled": false, + "new": true, + "status": "string", + "cancel_only": true, + "limit_only": true, + "post_only": true, + "trading_disabled": false, + "auction_mode": true, + "product_type": "SPOT", + "quote_currency_id": "USD", + "base_currency_id": "BTC", + "mid_market_price": "140.22", + "base_display_symbol": "BTC", + "quote_display_symbol": "USD" + } + """; + + return json; + } + + private string GetInvalidProductJsonString() + { + var json = + """ + { + "product_id": "BTC-USD", + "price": "140.21", + "price_percentage_change_24h": "9.43", + "volume_24h": "1908432", + "volume_percentage_change_24h": "9.43", + "base_increment": "0.00000001", + "quote_increment": "0.00000001", + "quote_min_size": "0.00000001", + "quote_max_size": "1000", + "base_min_size": "0.00000001", + "base_max_size": "1000", + "base_name": "Bitcoin", + "quote_name": "US Dollar", + "watched": "INVALID", + "is_disabled": false, + "new": true, + "status": "string", + "cancel_only": true, + "limit_only": true, + "post_only": true, + "trading_disabled": false, + "auction_mode": true, + "product_type": "SPOT", + "quote_currency_id": "USD", + "base_currency_id": "BTC", + "mid_market_price": "140.22", + "base_display_symbol": "BTC", + "quote_display_symbol": "USD" + } + """; + + return json; + } + + private string GetCandlesListJsonString() + { + var json = + """ + { + "candles": [ + { + "start": "1639508050", + "low": "140.21", + "high": "140.21", + "open": "140.21", + "close": "140.21", + "volume": "56437345" + } + ] + } + """; + + return json; + } + + private string GetInvalidCandlesListJsonString() + { + var json = + """ + { + "candles": + { + "start": "1639508050", + "low": "140.21", + "high": "140.21", + "open": "140.21", + "close": "140.21", + "volume": "56437345" + } + } + """; + + return json; + } + + private string GetMarketTradesListJsonString() + { + var json = + """ + { + "trades": [ + { + "trade_id": "34b080bf-fcfd-445a-832b-46b5ddc65601", + "product_id": "BTC-USD", + "price": "140.91", + "size": "4", + "time": "2021-05-31T09:59:59Z", + "side": "BUY", + "bid": "291.13", + "ask": "292.40" + } + ], + "best_bid": "291.13", + "best_ask": "292.40" + } + """; + + return json; + } + + private string GetInvalidMarketTradesListJsonString() + { + var json = + """ + { + "trades": + { + "trade_id": "34b080bf-fcfd-445a-832b-46b5ddc65601", + "product_id": "BTC-USD", + "price": "INVALID", + "size": "4", + "time": "2021-05-31T09:59:59Z", + "side": "SELL", + "bid": "291.13", + "ask": "292.40" + }, + "best_bid": "291.13", + "best_ask": "292.40" + } + """; + + return json; + } + + #endregion // Test Response Json + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/TransactionSummaryEndpointTests.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/TransactionSummaryEndpointTests.cs index aeb81de..abc766d 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/TransactionSummaryEndpointTests.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/Endpoints/TransactionSummaryEndpointTests.cs @@ -4,6 +4,7 @@ using CoinbaseAdvancedTradeClient.Models.Api.Common; using CoinbaseAdvancedTradeClient.Models.Api.TransactionSummaries; using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers; using Flurl.Http; using Flurl.Http.Testing; using Xunit; @@ -16,12 +17,7 @@ public class TransactionSummaryEndpointTests public TransactionSummaryEndpointTests() { - var config = new ApiClientConfig() - { - ApiKey = "key", - ApiSecret = "secret" - }; - + var config = TestConfigHelper.CreateTestApiConfig(); _testClient = new CoinbaseAdvancedTradeApiClient(config); } diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/TestHelpers/TestConfigHelper.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/TestHelpers/TestConfigHelper.cs new file mode 100644 index 0000000..5261085 --- /dev/null +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.UnitTests/TestHelpers/TestConfigHelper.cs @@ -0,0 +1,36 @@ +using CoinbaseAdvancedTradeClient.Models.Config; + +namespace CoinbaseAdvancedTradeClient.UnitTests.TestHelpers +{ + public static class TestConfigHelper + { + // Generate a valid Ed25519 key for testing purposes + public static string GenerateTestKeySecret() + { + var keyBytes = new byte[64]; + for (int i = 0; i < 64; i++) + { + keyBytes[i] = (byte)(i % 256); // Fill with test pattern + } + return Convert.ToBase64String(keyBytes); + } + + public static SecretApiKeyConfig CreateTestApiConfig() + { + return new SecretApiKeyConfig() + { + KeyName = "test-key", + KeySecret = GenerateTestKeySecret() + }; + } + + public static SecretApiKeyWebSocketConfig CreateTestWebSocketConfig() + { + return new SecretApiKeyWebSocketConfig() + { + KeyName = "test-key", + KeySecret = GenerateTestKeySecret() + }; + } + } +} \ No newline at end of file diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Authentication/SecretApiKeyAuthenticator.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Authentication/SecretApiKeyAuthenticator.cs new file mode 100644 index 0000000..7233325 --- /dev/null +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Authentication/SecretApiKeyAuthenticator.cs @@ -0,0 +1,108 @@ +using CoinbaseAdvancedTradeClient.Resources; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using System.Globalization; +using System.Text; + +namespace CoinbaseAdvancedTradeClient.Authentication +{ + public static class SecretApiKeyAuthenticator + { + public static string GenerateBearerJWT(string keyName, string keySecret, string requestMethod, string requestHost, string requestPath) + { + if (string.IsNullOrWhiteSpace(keyName)) throw new ArgumentException(ErrorMessages.ApiKeyRequired, nameof(keyName)); + if (string.IsNullOrWhiteSpace(keySecret)) throw new ArgumentException(ErrorMessages.ApiSecretRequired, nameof(keySecret)); + if (string.IsNullOrWhiteSpace(requestMethod)) throw new ArgumentNullException(nameof(requestMethod), ErrorMessages.RequestMethodRequired); + if (string.IsNullOrWhiteSpace(requestHost)) throw new ArgumentNullException(nameof(requestHost), ErrorMessages.RequestHostRequired); + if (string.IsNullOrWhiteSpace(requestPath)) throw new ArgumentNullException(nameof(requestPath), ErrorMessages.RequestPathRequired); + + // Decode the Ed25519 private key from base64 + byte[] decoded; + try + { + decoded = Convert.FromBase64String(keySecret); + } + catch (FormatException ex) + { + throw new ArgumentException(ErrorMessages.InvalidBase64KeyFormat, nameof(keySecret), ex); + } + + // Ed25519 keys are 64 bytes (32 bytes seed + 32 bytes public key) + if (decoded.Length != 64) + { + throw new ArgumentException(ErrorMessages.InvalidEd25519KeyLength, nameof(keySecret)); + } + + // Extract the seed (first 32 bytes) + byte[] seed = new byte[32]; + Array.Copy(decoded, 0, seed, 0, 32); + + // Create Ed25519 private key parameters + var privateKey = new Ed25519PrivateKeyParameters(seed, 0); + + // Create the URI + string uri = $"{requestMethod.ToUpperInvariant()} {requestHost}{requestPath}"; + + // Create header + var header = new Dictionary + { + { "alg", "EdDSA" }, + { "typ", "JWT" }, + { "kid", keyName }, + { "nonce", GenerateNonce() } + }; + + // Create payload with timing + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var payload = new Dictionary + { + { "sub", keyName }, + { "iss", "cdp" }, + { "aud", new[] { "cdp_service" } }, + { "nbf", now }, + { "exp", now + 120 }, // 2 minutes expiration + { "uri", uri } + }; + + // Encode header and payload + string headerJson = JsonConvert.SerializeObject(header); + string payloadJson = JsonConvert.SerializeObject(payload); + + string encodedHeader = Base64UrlEncode(Encoding.UTF8.GetBytes(headerJson)); + string encodedPayload = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson)); + + string message = $"{encodedHeader}.{encodedPayload}"; + + // Sign with Ed25519 + var signer = new Ed25519Signer(); + signer.Init(true, privateKey); + byte[] messageBytes = Encoding.UTF8.GetBytes(message); + signer.BlockUpdate(messageBytes, 0, messageBytes.Length); + byte[] signature = signer.GenerateSignature(); + + string encodedSignature = Base64UrlEncode(signature); + + return $"{message}.{encodedSignature}"; + } + + private static string GenerateNonce() + { + var random = new Random(); + var nonce = new char[16]; + for (int i = 0; i < 16; i++) + { + nonce[i] = (char)('0' + random.Next(10)); + } + return new string(nonce); + } + + private static string Base64UrlEncode(byte[] input) + { + return Convert.ToBase64String(input) + .Replace("+", "-") + .Replace("/", "_") + .Replace("=", ""); + } + } +} \ No newline at end of file diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeApiClient.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeApiClient.cs index 1bcf829..5acb9d5 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeApiClient.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeApiClient.cs @@ -1,48 +1,44 @@ using CoinbaseAdvancedTradeClient.Authentication; -using CoinbaseAdvancedTradeClient.Constants; using CoinbaseAdvancedTradeClient.Interfaces; using CoinbaseAdvancedTradeClient.Models.Api.Common; using CoinbaseAdvancedTradeClient.Models.Config; using CoinbaseAdvancedTradeClient.Resources; using Flurl.Http; -using Flurl.Http.Configuration; namespace CoinbaseAdvancedTradeClient { public partial class CoinbaseAdvancedTradeApiClient : FlurlClient, ICoinbaseAdvancedTradeApiClient { - private ApiClientConfig _config; + private readonly SecretApiKeyConfig _config; - public CoinbaseAdvancedTradeApiClient(ApiClientConfig config) + public CoinbaseAdvancedTradeApiClient(SecretApiKeyConfig config) { if (config == null) throw new ArgumentNullException(nameof(config), ErrorMessages.ApiConfigRequired); - if (string.IsNullOrWhiteSpace(config.ApiKey)) throw new ArgumentException(ErrorMessages.ApiKeyRequired, nameof(config.ApiKey)); - if (string.IsNullOrWhiteSpace(config.ApiSecret)) throw new ArgumentException(ErrorMessages.ApiSecretRequired, nameof(config.ApiSecret)); + if (string.IsNullOrWhiteSpace(config.KeyName)) throw new ArgumentException(ErrorMessages.ApiKeyRequired, nameof(config.KeyName)); + if (string.IsNullOrWhiteSpace(config.KeySecret)) throw new ArgumentException(ErrorMessages.ApiSecretRequired, nameof(config.KeySecret)); _config = config; - this.Configure(ApiKeyAuthentication); + this.BeforeCall(SecretApiKeyAuthentication); } #region Authentication - private void ApiKeyAuthentication(ClientFlurlHttpSettings settings) + private void SecretApiKeyAuthentication(FlurlCall call) { - async Task SetHeaders(FlurlCall http) - { - var body = http.RequestBody; - var method = http.Request.Verb.Method.ToUpperInvariant(); - var url = http.Request.Url.ToUri().AbsolutePath; - var timestamp = ApiKeyAuthenticator.GenerateTimestamp(); - var signature = ApiKeyAuthenticator.GenerateApiSignature(_config.ApiSecret, timestamp, method, url, body); - - http.Request - .WithHeader(RequestHeaders.AccessKey, _config.ApiKey) - .WithHeader(RequestHeaders.AccessSign, signature) - .WithHeader(RequestHeaders.AccessTimestamp, timestamp); - } - - settings.BeforeCallAsync = SetHeaders; + var method = call.Request.Verb.Method.ToUpperInvariant(); + var uri = call.Request.Url.ToUri(); + var requestHost = uri.Host; + var requestPath = uri.PathAndQuery; + + var jwt = SecretApiKeyAuthenticator.GenerateBearerJWT( + _config.KeyName, + _config.KeySecret, + method, + requestHost, + requestPath); + + call.Request.WithHeader("Authorization", $"Bearer {jwt}"); } #endregion // Authentication diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.csproj b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.csproj index cc63c12..c75339b 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.csproj +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient.csproj @@ -12,8 +12,9 @@ - - + + + diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeWebsocketClient.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeWebsocketClient.cs index 7ca4fc9..68798b5 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeWebsocketClient.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeWebsocketClient.cs @@ -1,211 +1,219 @@ -using CoinbaseAdvancedTradeClient.Authentication; -using CoinbaseAdvancedTradeClient.Constants; -using CoinbaseAdvancedTradeClient.Enums; -using CoinbaseAdvancedTradeClient.Interfaces; -using CoinbaseAdvancedTradeClient.Models.Config; -using CoinbaseAdvancedTradeClient.Models.WebSocket; -using CoinbaseAdvancedTradeClient.Models.WebSocket.Events; -using CoinbaseAdvancedTradeClient.Resources; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Security.Authentication; -using WebSocket4Net; - -namespace CoinbaseAdvancedTradeClient -{ - public class CoinbaseAdvancedTradeWebSocketClient : ICoinbaseAdvancedTradeWebSocketClient, IDisposable - { - private WebSocketClientConfig _config; - private WebSocket _socket; - - private Action _messageReceivedCallback; - private Action? _openedCallback; - private Action? _closedCallback; - private Action? _errorCallback; - - public bool IsConnected => _socket?.State == WebSocketState.Open; - - public CoinbaseAdvancedTradeWebSocketClient(WebSocketClientConfig config) - { - if (config == null) throw new ArgumentNullException(nameof(config), ErrorMessages.ApiConfigRequired); - if (string.IsNullOrWhiteSpace(config.ApiKey)) throw new ArgumentException(ErrorMessages.ApiKeyRequired, nameof(config.ApiKey)); - if (string.IsNullOrWhiteSpace(config.ApiSecret)) throw new ArgumentException(ErrorMessages.ApiSecretRequired, nameof(config.ApiSecret)); - - _config = config; - } - - #region Connection - - public async Task ConnectAsync(Action messageReceivedCallback, Action? openedCallback = null, Action? closedCallback = null, Action? errorCallback = null) - { - if (messageReceivedCallback == null) throw new ArgumentNullException(nameof(messageReceivedCallback), ErrorMessages.MessageReceivedCallbackRequired); - - _messageReceivedCallback = messageReceivedCallback; - _openedCallback = openedCallback; - _closedCallback = closedCallback; - _errorCallback = errorCallback; - - if (_socket != null) - { - Disconnect(); - } - - _socket = new WebSocket(_config.WebSocketUrl); - _socket.Security.EnabledSslProtocols = SslProtocols.Tls12; - - _socket.Opened += Socket_Opened; - _socket.Closed += Socket_Closed; - _socket.Error += Socket_Error; - _socket.MessageReceived += Socket_MessageReceived; - - return await _socket.OpenAsync(); - } - - public void Disconnect() - { - if (_socket != null && _socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Closing) - { - _socket.Close(); - } - } - - #endregion // Connection - - #region Subscription - - public void Subscribe(string channel, List productIds) - { - if (string.IsNullOrWhiteSpace(channel) || !WebSocketChannels.WebSocketChannelList.Contains(channel)) throw new ArgumentException(ErrorMessages.ChannelRequired, nameof(channel)); - if (productIds == null || !productIds.Any()) throw new ArgumentNullException(nameof(productIds), ErrorMessages.ProductIdRequired); - - if (!IsConnected) throw new InvalidOperationException(ErrorMessages.WebSocketMustBeConnected); - - var timestamp = ApiKeyAuthenticator.GenerateTimestamp(); - var signature = ApiKeyAuthenticator.GenerateWebSocketSignature(_config.ApiSecret, timestamp, channel, productIds); - - var subscriptionMessage = new SubscriptionMessage - { - ApiKey = _config.ApiKey, - Channel = channel, - ProductIds = productIds, - Signature = signature, - Timestamp = timestamp, - Type = SubscriptionType.Subscribe, - }; - - var subscribe = JsonConvert.SerializeObject(subscriptionMessage); - - _socket.Send(subscribe); - } - - public void Unsubscribe(string channel, List productIds) - { - if (string.IsNullOrWhiteSpace(channel) || !WebSocketChannels.WebSocketChannelList.Contains(channel)) throw new ArgumentException(ErrorMessages.ChannelRequired, nameof(channel)); - if (productIds == null || !productIds.Any()) throw new ArgumentNullException(nameof(productIds), ErrorMessages.ProductIdRequired); - - if (!IsConnected) throw new InvalidOperationException(ErrorMessages.WebSocketMustBeConnected); - - var timestamp = ApiKeyAuthenticator.GenerateTimestamp(); - var signature = ApiKeyAuthenticator.GenerateWebSocketSignature(_config.ApiSecret, timestamp, channel, productIds); - - var unsubscribeMessage = new SubscriptionMessage - { - ApiKey = _config.ApiKey, - Channel = channel, - ProductIds = productIds, - Signature = signature, - Timestamp = timestamp, - Type = SubscriptionType.Unsubscribe - }; - - var unsubscribe = JsonConvert.SerializeObject(unsubscribeMessage); - - _socket.Send(unsubscribe); - } - - #endregion // Subscription - - #region Event Handlers - - private void Socket_Opened(object? sender, EventArgs e) - { - if (_openedCallback != null) - { - _openedCallback.Invoke(sender, e); - } - } - - private void Socket_Closed(object? sender, EventArgs e) - { - if (_closedCallback != null) - { - _closedCallback.Invoke(sender, e); - } - } - - private void Socket_Error(object? sender, SuperSocket.ClientEngine.ErrorEventArgs e) - { - if (_errorCallback != null) - { - _errorCallback.Invoke(e.Exception); - } - else - { - throw e.Exception; - } - } - - private void Socket_MessageReceived(object? sender, MessageReceivedEventArgs e) - { - var parsed = ParseWebSocketMessage(e.Message, out object message); - - _messageReceivedCallback.Invoke(message, parsed); - } - - private bool ParseWebSocketMessage(string json, out object message) - { - var obj = JObject.Parse(json); - - var channel = obj[WebSocketChannels.Channel]?.Value(); - - switch (channel) - { - case WebSocketChannels.MarketTrades: - message = obj.ToObject>(); - return true; - case WebSocketChannels.Status: - message = obj.ToObject>(); - return true; - case WebSocketChannels.Ticker: - case WebSocketChannels.TickerBatch: - message = obj.ToObject>(); - return true; - case WebSocketChannels.Level2Data: - message = obj.ToObject>(); - return true; - case WebSocketChannels.User: - message = obj.ToObject>(); - return true; - case WebSocketChannels.Subscriptions: - message = obj.ToObject>(); - return true; - default: - message = json; - return false; - } - } - - #endregion // Event Handlers - - #region Dispose - - public void Dispose() - { - Disconnect(); - - _socket?.Dispose(); - } - - #endregion // Dispose - } -} +using CoinbaseAdvancedTradeClient.Authentication; +using CoinbaseAdvancedTradeClient.Constants; +using CoinbaseAdvancedTradeClient.Enums; +using CoinbaseAdvancedTradeClient.Interfaces; +using CoinbaseAdvancedTradeClient.Models.Config; +using CoinbaseAdvancedTradeClient.Models.WebSocket; +using CoinbaseAdvancedTradeClient.Models.WebSocket.Events; +using CoinbaseAdvancedTradeClient.Resources; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Security.Authentication; +using WebSocket4Net; + +namespace CoinbaseAdvancedTradeClient +{ + public class CoinbaseAdvancedTradeWebSocketClient : ICoinbaseAdvancedTradeWebSocketClient, IDisposable + { + private readonly SecretApiKeyWebSocketConfig _config; + private WebSocket _socket; + + private Action _messageReceivedCallback; + private Action? _openedCallback; + private Action? _closedCallback; + private Action? _errorCallback; + + public bool IsConnected => _socket?.State == WebSocketState.Open; + + public CoinbaseAdvancedTradeWebSocketClient(SecretApiKeyWebSocketConfig config) + { + if (config == null) throw new ArgumentNullException(nameof(config), ErrorMessages.ApiConfigRequired); + if (string.IsNullOrWhiteSpace(config.KeyName)) throw new ArgumentException(ErrorMessages.ApiKeyRequired, nameof(config.KeyName)); + if (string.IsNullOrWhiteSpace(config.KeySecret)) throw new ArgumentException(ErrorMessages.ApiSecretRequired, nameof(config.KeySecret)); + + _config = config; + } + + #region Connection + + public async Task ConnectAsync(Action messageReceivedCallback, Action? openedCallback = null, Action? closedCallback = null, Action? errorCallback = null) + { + if (messageReceivedCallback == null) throw new ArgumentNullException(nameof(messageReceivedCallback), ErrorMessages.MessageReceivedCallbackRequired); + + _messageReceivedCallback = messageReceivedCallback; + _openedCallback = openedCallback; + _closedCallback = closedCallback; + _errorCallback = errorCallback; + + if (_socket != null) + { + Disconnect(); + } + + _socket = new WebSocket(_config.WebSocketUrl); + _socket.Security.EnabledSslProtocols = SslProtocols.Tls12; + + _socket.Opened += Socket_Opened; + _socket.Closed += Socket_Closed; + _socket.Error += Socket_Error; + _socket.MessageReceived += Socket_MessageReceived; + + return await _socket.OpenAsync(); + } + + public void Disconnect() + { + if (_socket != null && _socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Closing) + { + _socket.Close(); + } + } + + #endregion // Connection + + #region Subscription + + public void Subscribe(string channel, List productIds) + { + if (string.IsNullOrWhiteSpace(channel) || !WebSocketChannels.WebSocketChannelList.Contains(channel)) throw new ArgumentException(ErrorMessages.ChannelRequired, nameof(channel)); + if (productIds == null || !productIds.Any()) throw new ArgumentNullException(nameof(productIds), ErrorMessages.ProductIdRequired); + + if (!IsConnected) throw new InvalidOperationException(ErrorMessages.WebSocketMustBeConnected); + + // Generate JWT for WebSocket subscription + var uri = new Uri(_config.WebSocketUrl); + var jwt = SecretApiKeyAuthenticator.GenerateBearerJWT( + _config.KeyName, + _config.KeySecret, + "GET", + uri.Host, + uri.PathAndQuery); + + var subscriptionMessage = new SubscriptionMessage + { + Type = SubscriptionType.Subscribe, + Channel = channel, + ProductIds = productIds, + Jwt = jwt + }; + + var subscribe = JsonConvert.SerializeObject(subscriptionMessage); + + _socket.Send(subscribe); + } + + public void Unsubscribe(string channel, List productIds) + { + if (string.IsNullOrWhiteSpace(channel) || !WebSocketChannels.WebSocketChannelList.Contains(channel)) throw new ArgumentException(ErrorMessages.ChannelRequired, nameof(channel)); + if (productIds == null || !productIds.Any()) throw new ArgumentNullException(nameof(productIds), ErrorMessages.ProductIdRequired); + + if (!IsConnected) throw new InvalidOperationException(ErrorMessages.WebSocketMustBeConnected); + + // Generate JWT for WebSocket unsubscription + var uri = new Uri(_config.WebSocketUrl); + var jwt = SecretApiKeyAuthenticator.GenerateBearerJWT( + _config.KeyName, + _config.KeySecret, + "GET", + uri.Host, + uri.PathAndQuery); + + var unsubscribeMessage = new SubscriptionMessage + { + Type = SubscriptionType.Unsubscribe, + Channel = channel, + ProductIds = productIds, + Jwt = jwt + }; + + var unsubscribe = JsonConvert.SerializeObject(unsubscribeMessage); + + _socket.Send(unsubscribe); + } + + #endregion // Subscription + + #region Event Handlers + + private void Socket_Opened(object? sender, EventArgs e) + { + if (_openedCallback != null) + { + _openedCallback.Invoke(sender, e); + } + } + + private void Socket_Closed(object? sender, EventArgs e) + { + if (_closedCallback != null) + { + _closedCallback.Invoke(sender, e); + } + } + + private void Socket_Error(object? sender, SuperSocket.ClientEngine.ErrorEventArgs e) + { + if (_errorCallback != null) + { + _errorCallback.Invoke(e.Exception); + } + else + { + throw e.Exception; + } + } + + private void Socket_MessageReceived(object? sender, MessageReceivedEventArgs e) + { + var parsed = ParseWebSocketMessage(e.Message, out object message); + + _messageReceivedCallback.Invoke(message, parsed); + } + + private bool ParseWebSocketMessage(string json, out object message) + { + var obj = JObject.Parse(json); + + var channel = obj[WebSocketChannels.Channel]?.Value(); + + switch (channel) + { + case WebSocketChannels.MarketTrades: + message = obj.ToObject>(); + return true; + case WebSocketChannels.Status: + message = obj.ToObject>(); + return true; + case WebSocketChannels.Ticker: + case WebSocketChannels.TickerBatch: + message = obj.ToObject>(); + return true; + case WebSocketChannels.Level2Data: + message = obj.ToObject>(); + return true; + case WebSocketChannels.User: + message = obj.ToObject>(); + return true; + case WebSocketChannels.Subscriptions: + message = obj.ToObject>(); + return true; + default: + message = json; + return false; + } + } + + #endregion // Event Handlers + + #region Dispose + + public void Dispose() + { + Disconnect(); + + _socket?.Dispose(); + } + + #endregion // Dispose + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/AccountsEndpoint.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/AccountsEndpoint.cs index d96c900..c49c48d 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/AccountsEndpoint.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/AccountsEndpoint.cs @@ -1,67 +1,65 @@ -using CoinbaseAdvancedTradeClient.Constants; -using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; -using CoinbaseAdvancedTradeClient.Models.Api.Accounts; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Pages; -using CoinbaseAdvancedTradeClient.Resources; -using Flurl; -using Flurl.Http; - -namespace CoinbaseAdvancedTradeClient -{ - public partial class CoinbaseAdvancedTradeApiClient : IAccountsEndpoint - { - public IAccountsEndpoint Accounts => this; - - async Task> IAccountsEndpoint.GetListAccountsAsync(int? limit = null, string cursor = null) - { - var response = new ApiResponse(); - - try - { - if (limit != null && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); - - var accountsPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.AccountsEndpoint) - .SetQueryParam(RequestParameters.Limit, limit) - .SetQueryParam(RequestParameters.Cursor, cursor) - .GetJsonAsync(); - - response.Data = accountsPage; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IAccountsEndpoint.GetAccountAsync(string accountId) - { - var response = new ApiResponse(); - - try - { - if (string.IsNullOrWhiteSpace(accountId)) throw new ArgumentNullException(nameof(accountId), ErrorMessages.AccountIdRequired); - - var accountsPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.AccountsEndpoint) - .AppendPathSegment(accountId) - .GetJsonAsync(); - - response.Data = accountsPage.Account; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - } -} +using CoinbaseAdvancedTradeClient.Constants; +using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; +using CoinbaseAdvancedTradeClient.Models.Api.Accounts; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Pages; +using CoinbaseAdvancedTradeClient.Resources; +using Flurl; +using Flurl.Http; + +namespace CoinbaseAdvancedTradeClient +{ + public partial class CoinbaseAdvancedTradeApiClient : IAccountsEndpoint + { + public IAccountsEndpoint Accounts => this; + + async Task> IAccountsEndpoint.GetListAccountsAsync(int? limit = null, string cursor = null) + { + var response = new ApiResponse(); + + try + { + if (limit != null && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); + + var accountsPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.AccountsEndpoint) + .SetQueryParam(RequestParameters.Limit, limit) + .SetQueryParam(RequestParameters.Cursor, cursor) + .GetJsonAsync(); + + response.Data = accountsPage; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IAccountsEndpoint.GetAccountAsync(string accountId) + { + var response = new ApiResponse(); + + try + { + if (string.IsNullOrWhiteSpace(accountId)) throw new ArgumentNullException(nameof(accountId), ErrorMessages.AccountIdRequired); + + var accountsPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.AccountsEndpoint) + .AppendPathSegment(accountId) + .GetJsonAsync(); + + response.Data = accountsPage.Account; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/OrdersEndpoint.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/OrdersEndpoint.cs index 23edaaa..3d6961e 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/OrdersEndpoint.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/OrdersEndpoint.cs @@ -1,261 +1,257 @@ -using CoinbaseAdvancedTradeClient.Constants; -using CoinbaseAdvancedTradeClient.Enums; -using CoinbaseAdvancedTradeClient.Extensions; -using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Api.Orders; -using CoinbaseAdvancedTradeClient.Models.Pages; -using CoinbaseAdvancedTradeClient.Resources; -using Flurl.Http; - -namespace CoinbaseAdvancedTradeClient -{ - public partial class CoinbaseAdvancedTradeApiClient : IOrdersEndpoint - { - public IOrdersEndpoint Orders => this; - - #region GET - - async Task> IOrdersEndpoint.GetListFillsAsync(string? orderId = null, string? productId = null, - DateTimeOffset? start = null, DateTimeOffset? end = null, int? limit = null, string? cursor = null) - { - var response = new ApiResponse(); - - try - { - if (limit != null && limit != 0 && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); - - if (start.Equals(DateTimeOffset.MinValue)) start = null; - if (end.Equals(DateTimeOffset.MinValue)) end = null; - - var fillsPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.OrdersHistoricalFillsEndpoint) - .SetQueryParam(RequestParameters.OrderId, orderId) - .SetQueryParam(RequestParameters.ProductId, productId) - .SetQueryParam(RequestParameters.StartSequenceTimestamp, start) - .SetQueryParam(RequestParameters.EndSequenceTimestamp, end) - .SetQueryParam(RequestParameters.Limit, limit) - .SetQueryParam(RequestParameters.Cursor, cursor) - .GetJsonAsync(); - - response.Data = fillsPage; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IOrdersEndpoint.GetListOrdersAsync(string? productId = null, ICollection? orderStatuses = null, int? limit = null, - DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, string? userNativeCurrency = null, OrderType? orderType = null, OrderSide? orderSide = null, - string? cursor = null, ProductType? productType = null, OrderPlacementSource? orderPlacementSource = null) - { - var response = new ApiResponse(); - - try - { - if (limit != null && limit != 0 && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); - - if (orderStatuses != null && !orderStatuses.Any()) orderStatuses = null; - if (startDate.Equals(DateTimeOffset.MinValue)) startDate = null; - if (endDate.Equals(DateTimeOffset.MinValue)) endDate = null; - - var ordersPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.OrdersHistoricalBatchEndpoint) - .SetQueryParam(RequestParameters.ProductId, productId) - .SetQueryParam(RequestParameters.OrderStatus, orderStatuses?.ToArray()) - .SetQueryParam(RequestParameters.Limit, limit) - .SetQueryParam(RequestParameters.StartDate, startDate) - .SetQueryParam(RequestParameters.EndDate, endDate) - .SetQueryParam(RequestParameters.UserNativeCurrency, userNativeCurrency) - .SetQueryParam(RequestParameters.OrderType, orderType?.GetEnumMemberValue()) - .SetQueryParam(RequestParameters.OrderSide, orderSide?.GetEnumMemberValue()) - .SetQueryParam(RequestParameters.Cursor, cursor) - .SetQueryParam(RequestParameters.ProductType, productType?.GetEnumMemberValue()) - .SetQueryParam(RequestParameters.OrderPlacementSource, orderPlacementSource?.GetEnumMemberValue()) - .GetJsonAsync(); - - response.Data = ordersPage; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IOrdersEndpoint.GetOrderAsync(string orderId) - { - var response = new ApiResponse(); - - try - { - if (string.IsNullOrWhiteSpace(orderId)) throw new ArgumentNullException(nameof(orderId), ErrorMessages.OrderIdRequired); - - var ordersPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.OrdersHistoricalEndpoint) - .AppendPathSegment(orderId) - .GetJsonAsync(); - - response.Data = ordersPage.Order; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - #endregion // GET - - #region POST - - async Task> IOrdersEndpoint.PostCreateOrderAsync(CreateOrderParameters createOrder, CancellationToken cancellationToken = default) - { - var response = new ApiResponse(); - - try - { - if (createOrder == null) throw new ArgumentNullException(nameof(createOrder), ErrorMessages.OrderParametersRequired); - if (string.IsNullOrWhiteSpace(createOrder.ProductId)) throw new ArgumentException(ErrorMessages.ProductIdRequired, nameof(createOrder.ProductId)); - if (createOrder.OrderConfiguration == null) throw new ArgumentException(ErrorMessages.OrderConfigurationInvalid, nameof(createOrder.OrderConfiguration)); - - if (string.IsNullOrWhiteSpace(createOrder.ClientOrderId)) - { - createOrder.ClientOrderId = Guid.NewGuid().ToString(); - } - - var createOrderResponse = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.OrdersEndpoint) - .PostJsonAsync(createOrder, cancellationToken) - .ReceiveJson(); - - response.Data = createOrderResponse; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IOrdersEndpoint.PostCancelOrdersAsync(CancelOrdersParameters cancelOrders, CancellationToken cancellationToken = default) - { - var response = new ApiResponse(); - - try - { - if (cancelOrders == null) throw new ArgumentNullException(nameof(cancelOrders), ErrorMessages.OrderParametersRequired); - if (cancelOrders.OrderIds == null || !cancelOrders.OrderIds.Any()) throw new ArgumentNullException(nameof(cancelOrders), ErrorMessages.OrderIdRequired); - - var cancelOrderResponse = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.OrdersBatchCancelEndpoint) - .PostJsonAsync(cancelOrders, cancellationToken) - .ReceiveJson(); - - response.Data = cancelOrderResponse; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - #endregion // POST - - #region Create Order Helper Methods - - async Task> IOrdersEndpoint.CreateMarketOrderAsync(OrderSide orderSide, - string productId, decimal amount, - string clientOrderId = null, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); - if (amount <= 0) throw new ArgumentException(ErrorMessages.AmountParameterRange, nameof(amount)); - - var createOrderParameters = new CreateOrderParameters - { - ClientOrderId = clientOrderId, - ProductId = productId, - Side = orderSide, - }; - - createOrderParameters.BuildMarketIocConfiguration(amount, orderSide); - - return await Orders.PostCreateOrderAsync(createOrderParameters, cancellationToken); - } - - async Task> IOrdersEndpoint.CreateLimitOrderAsync(TimeInForce timeInForce, OrderSide orderSide, - string productId, decimal amount, decimal limitPrice, bool postOnly, DateTimeOffset endTime, - string clientOrderId = null, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); - if (amount <= 0) throw new ArgumentException(ErrorMessages.AmountParameterRange, nameof(amount)); - if (limitPrice <= 0) throw new ArgumentException(ErrorMessages.LimitPriceParameterRange, nameof(limitPrice)); - - var createOrderParameters = new CreateOrderParameters - { - ClientOrderId = clientOrderId, - ProductId = productId, - Side = orderSide, - }; - - if (timeInForce.Equals(TimeInForce.GoodUntilCancelled)) - { - createOrderParameters.BuildLimitGtcConfiguration(amount, limitPrice, postOnly); - } - else - { - createOrderParameters.BuildLimitGtdConfiguration(amount, limitPrice, postOnly, endTime); - } - - return await Orders.PostCreateOrderAsync(createOrderParameters, cancellationToken); - } - - async Task> IOrdersEndpoint.CreateStopLimitOrderAsync(TimeInForce timeInForce, OrderSide orderSide, - string productId, decimal amount, decimal limitPrice, decimal stopPrice, StopDirection stopDirection, DateTimeOffset endTime, - string clientOrderId = null, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); - if (amount <= 0) throw new ArgumentException(ErrorMessages.AmountParameterRange, nameof(amount)); - if (limitPrice <= 0) throw new ArgumentException(ErrorMessages.LimitPriceParameterRange, nameof(limitPrice)); - if (stopPrice <= 0) throw new ArgumentException(ErrorMessages.StopPriceParameterRange, nameof(stopPrice)); - - var createOrderParameters = new CreateOrderParameters - { - ClientOrderId = clientOrderId, - ProductId = productId, - Side = orderSide, - }; - - if (timeInForce.Equals(TimeInForce.GoodUntilCancelled)) - { - createOrderParameters.BuildStopLimitGtcConfiguration(amount, limitPrice, stopPrice, stopDirection); - } - else - { - createOrderParameters.BuildStopLimitGtdConfiguration(amount, limitPrice, stopPrice, stopDirection, endTime); - } - - return await Orders.PostCreateOrderAsync(createOrderParameters, cancellationToken); - } - - #endregion // Create Order Helper Methods - } -} +using CoinbaseAdvancedTradeClient.Constants; +using CoinbaseAdvancedTradeClient.Enums; +using CoinbaseAdvancedTradeClient.Extensions; +using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Api.Orders; +using CoinbaseAdvancedTradeClient.Models.Pages; +using CoinbaseAdvancedTradeClient.Resources; +using Flurl; +using Flurl.Http; + +namespace CoinbaseAdvancedTradeClient +{ + public partial class CoinbaseAdvancedTradeApiClient : IOrdersEndpoint + { + public IOrdersEndpoint Orders => this; + + #region GET + + async Task> IOrdersEndpoint.GetListFillsAsync(string? orderId = null, string? productId = null, + DateTimeOffset? start = null, DateTimeOffset? end = null, int? limit = null, string? cursor = null) + { + var response = new ApiResponse(); + + try + { + if (limit != null && limit != 0 && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); + + if (start.Equals(DateTimeOffset.MinValue)) start = null; + if (end.Equals(DateTimeOffset.MinValue)) end = null; + + var fillsPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.OrdersHistoricalFillsEndpoint) + .SetQueryParam(RequestParameters.OrderId, orderId) + .SetQueryParam(RequestParameters.ProductId, productId) + .SetQueryParam(RequestParameters.StartSequenceTimestamp, start) + .SetQueryParam(RequestParameters.EndSequenceTimestamp, end) + .SetQueryParam(RequestParameters.Limit, limit) + .SetQueryParam(RequestParameters.Cursor, cursor) + .GetJsonAsync(); + + response.Data = fillsPage; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IOrdersEndpoint.GetListOrdersAsync(string? productId = null, ICollection? orderStatuses = null, int? limit = null, + DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, string? userNativeCurrency = null, OrderType? orderType = null, OrderSide? orderSide = null, + string? cursor = null, ProductType? productType = null, OrderPlacementSource? orderPlacementSource = null) + { + var response = new ApiResponse(); + + try + { + if (limit != null && limit != 0 && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); + + if (orderStatuses != null && !orderStatuses.Any()) orderStatuses = null; + if (startDate.Equals(DateTimeOffset.MinValue)) startDate = null; + if (endDate.Equals(DateTimeOffset.MinValue)) endDate = null; + + var ordersPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.OrdersHistoricalBatchEndpoint) + .SetQueryParam(RequestParameters.ProductId, productId) + .SetQueryParam(RequestParameters.OrderStatus, orderStatuses?.ToArray()) + .SetQueryParam(RequestParameters.Limit, limit) + .SetQueryParam(RequestParameters.StartDate, startDate) + .SetQueryParam(RequestParameters.EndDate, endDate) + .SetQueryParam(RequestParameters.UserNativeCurrency, userNativeCurrency) + .SetQueryParam(RequestParameters.OrderType, orderType?.GetEnumMemberValue()) + .SetQueryParam(RequestParameters.OrderSide, orderSide?.GetEnumMemberValue()) + .SetQueryParam(RequestParameters.Cursor, cursor) + .SetQueryParam(RequestParameters.ProductType, productType?.GetEnumMemberValue()) + .SetQueryParam(RequestParameters.OrderPlacementSource, orderPlacementSource?.GetEnumMemberValue()) + .GetJsonAsync(); + + response.Data = ordersPage; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IOrdersEndpoint.GetOrderAsync(string orderId) + { + var response = new ApiResponse(); + + try + { + if (string.IsNullOrWhiteSpace(orderId)) throw new ArgumentNullException(nameof(orderId), ErrorMessages.OrderIdRequired); + + var ordersPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.OrdersHistoricalEndpoint) + .AppendPathSegment(orderId) + .GetJsonAsync(); + + response.Data = ordersPage.Order; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + #endregion // GET + + #region POST + + async Task> IOrdersEndpoint.PostCreateOrderAsync(CreateOrderParameters createOrder, CancellationToken cancellationToken = default) + { + var response = new ApiResponse(); + + try + { + if (createOrder == null) throw new ArgumentNullException(nameof(createOrder), ErrorMessages.OrderParametersRequired); + if (string.IsNullOrWhiteSpace(createOrder.ProductId)) throw new ArgumentException(ErrorMessages.ProductIdRequired, nameof(createOrder.ProductId)); + if (createOrder.OrderConfiguration == null) throw new ArgumentException(ErrorMessages.OrderConfigurationInvalid, nameof(createOrder.OrderConfiguration)); + + if (string.IsNullOrWhiteSpace(createOrder.ClientOrderId)) + { + createOrder.ClientOrderId = Guid.NewGuid().ToString(); + } + + var createOrderResponse = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.OrdersEndpoint) + .PostJsonAsync(createOrder, HttpCompletionOption.ResponseContentRead, cancellationToken) + .ReceiveJson(); + + response.Data = createOrderResponse; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IOrdersEndpoint.PostCancelOrdersAsync(CancelOrdersParameters cancelOrders, CancellationToken cancellationToken = default) + { + var response = new ApiResponse(); + + try + { + if (cancelOrders == null) throw new ArgumentNullException(nameof(cancelOrders), ErrorMessages.OrderParametersRequired); + if (cancelOrders.OrderIds == null || !cancelOrders.OrderIds.Any()) throw new ArgumentNullException(nameof(cancelOrders), ErrorMessages.OrderIdRequired); + + var cancelOrderResponse = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.OrdersBatchCancelEndpoint) + .PostJsonAsync(cancelOrders, HttpCompletionOption.ResponseContentRead, cancellationToken) + .ReceiveJson(); + + response.Data = cancelOrderResponse; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + #endregion // POST + + #region Create Order Helper Methods + + async Task> IOrdersEndpoint.CreateMarketOrderAsync(OrderSide orderSide, + string productId, decimal amount, + string clientOrderId = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); + if (amount <= 0) throw new ArgumentException(ErrorMessages.AmountParameterRange, nameof(amount)); + + var createOrderParameters = new CreateOrderParameters + { + ClientOrderId = clientOrderId, + ProductId = productId, + Side = orderSide, + }; + + createOrderParameters.BuildMarketIocConfiguration(amount, orderSide); + + return await Orders.PostCreateOrderAsync(createOrderParameters, cancellationToken); + } + + async Task> IOrdersEndpoint.CreateLimitOrderAsync(TimeInForce timeInForce, OrderSide orderSide, + string productId, decimal amount, decimal limitPrice, bool postOnly, DateTimeOffset endTime, + string clientOrderId = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); + if (amount <= 0) throw new ArgumentException(ErrorMessages.AmountParameterRange, nameof(amount)); + if (limitPrice <= 0) throw new ArgumentException(ErrorMessages.LimitPriceParameterRange, nameof(limitPrice)); + + var createOrderParameters = new CreateOrderParameters + { + ClientOrderId = clientOrderId, + ProductId = productId, + Side = orderSide, + }; + + if (timeInForce.Equals(TimeInForce.GoodUntilCancelled)) + { + createOrderParameters.BuildLimitGtcConfiguration(amount, limitPrice, postOnly); + } + else + { + createOrderParameters.BuildLimitGtdConfiguration(amount, limitPrice, postOnly, endTime); + } + + return await Orders.PostCreateOrderAsync(createOrderParameters, cancellationToken); + } + + async Task> IOrdersEndpoint.CreateStopLimitOrderAsync(TimeInForce timeInForce, OrderSide orderSide, + string productId, decimal amount, decimal limitPrice, decimal stopPrice, StopDirection stopDirection, DateTimeOffset endTime, + string clientOrderId = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); + if (amount <= 0) throw new ArgumentException(ErrorMessages.AmountParameterRange, nameof(amount)); + if (limitPrice <= 0) throw new ArgumentException(ErrorMessages.LimitPriceParameterRange, nameof(limitPrice)); + if (stopPrice <= 0) throw new ArgumentException(ErrorMessages.StopPriceParameterRange, nameof(stopPrice)); + + var createOrderParameters = new CreateOrderParameters + { + ClientOrderId = clientOrderId, + ProductId = productId, + Side = orderSide, + }; + + if (timeInForce.Equals(TimeInForce.GoodUntilCancelled)) + { + createOrderParameters.BuildStopLimitGtcConfiguration(amount, limitPrice, stopPrice, stopDirection); + } + else + { + createOrderParameters.BuildStopLimitGtdConfiguration(amount, limitPrice, stopPrice, stopDirection, endTime); + } + + return await Orders.PostCreateOrderAsync(createOrderParameters, cancellationToken); + } + + #endregion // Create Order Helper Methods + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/ProductsEndpoint.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/ProductsEndpoint.cs index 3eabec9..eba82d7 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/ProductsEndpoint.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/ProductsEndpoint.cs @@ -1,129 +1,126 @@ -using CoinbaseAdvancedTradeClient.Constants; -using CoinbaseAdvancedTradeClient.Enums; -using CoinbaseAdvancedTradeClient.Extensions; -using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Api.Products; -using CoinbaseAdvancedTradeClient.Models.Pages; -using CoinbaseAdvancedTradeClient.Resources; -using Flurl.Http; - -namespace CoinbaseAdvancedTradeClient -{ - public partial class CoinbaseAdvancedTradeApiClient : IProductsEndpoint - { - public IProductsEndpoint Products => this; - - async Task> IProductsEndpoint.GetListProductsAsync(int? limit, int? offset, ProductType productType) - { - var response = new ApiResponse(); - - try - { - if (limit != null && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); - if (offset != null && (offset < 0)) throw new ArgumentException(ErrorMessages.OffsetParameterRange, nameof(offset)); - - var productsPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.ProductsEndpoint) - .SetQueryParam(RequestParameters.Limit, limit) - .SetQueryParam(RequestParameters.Offset, offset) - .SetQueryParam(RequestParameters.ProductType, productType.GetEnumMemberValue()) - .GetJsonAsync(); - - response.Data = productsPage; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IProductsEndpoint.GetProductAsync(string productId) - { - var response = new ApiResponse(); - - try - { - if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); - - var product = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.ProductsEndpoint) - .AppendPathSegment(productId) - .GetJsonAsync(); - - response.Data = product; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IProductsEndpoint.GetProductCandlesAsync(string productId, DateTimeOffset start, DateTimeOffset end, CandleGranularity granularity) - { - var response = new ApiResponse(); - - try - { - if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); - if (start.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.StartDateRequired, nameof(start)); - if (end.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.EndDateRequired, nameof(end)); - - var candlesPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.ProductsEndpoint) - .AppendPathSegment(productId) - .AppendPathSegment(ApiEndpoints.CandlesEndpoint) - .SetQueryParam(RequestParameters.Start, start.ToUnixTimeSeconds()) - .SetQueryParam(RequestParameters.End, end.ToUnixTimeSeconds()) - .SetQueryParam(RequestParameters.Granularity, granularity.GetEnumMemberValue()) - .GetJsonAsync(); - - response.Data = candlesPage; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - - async Task> IProductsEndpoint.GetMarketTradesAsync(string productId, int limit) - { - var response = new ApiResponse(); - - try - { - if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); - if (limit < 1 || limit > 250) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); - - var tradesPage = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.ProductsEndpoint) - .AppendPathSegment(productId) - .AppendPathSegment(ApiEndpoints.TickerEndpoint) - .SetQueryParam(RequestParameters.Limit, limit) - .GetJsonAsync(); - - response.Data = tradesPage; - response.Success = true; - } - catch (Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - } -} +using CoinbaseAdvancedTradeClient.Constants; +using CoinbaseAdvancedTradeClient.Enums; +using CoinbaseAdvancedTradeClient.Extensions; +using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Api.Products; +using CoinbaseAdvancedTradeClient.Models.Pages; +using CoinbaseAdvancedTradeClient.Resources; +using Flurl; +using Flurl.Http; + +namespace CoinbaseAdvancedTradeClient +{ + public partial class CoinbaseAdvancedTradeApiClient : IProductsEndpoint + { + public IProductsEndpoint Products => this; + + async Task> IProductsEndpoint.GetListProductsAsync(int? limit, int? offset, ProductType productType) + { + var response = new ApiResponse(); + + try + { + if (limit != null && (limit < 1 || limit > 250)) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); + if (offset != null && (offset < 0)) throw new ArgumentException(ErrorMessages.OffsetParameterRange, nameof(offset)); + + var productsPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.ProductsEndpoint) + .SetQueryParam(RequestParameters.Limit, limit) + .SetQueryParam(RequestParameters.Offset, offset) + .SetQueryParam(RequestParameters.ProductType, productType.GetEnumMemberValue()) + .GetJsonAsync(); + + response.Data = productsPage; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IProductsEndpoint.GetProductAsync(string productId) + { + var response = new ApiResponse(); + + try + { + if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); + + var product = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.ProductsEndpoint) + .AppendPathSegment(productId) + .GetJsonAsync(); + + response.Data = product; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IProductsEndpoint.GetProductCandlesAsync(string productId, DateTimeOffset start, DateTimeOffset end, CandleGranularity granularity) + { + var response = new ApiResponse(); + + try + { + if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); + if (start.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.StartDateRequired, nameof(start)); + if (end.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.EndDateRequired, nameof(end)); + + var candlesPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.ProductsEndpoint) + .AppendPathSegment(productId) + .AppendPathSegment(ApiEndpoints.CandlesEndpoint) + .SetQueryParam(RequestParameters.Start, start.ToUnixTimeSeconds()) + .SetQueryParam(RequestParameters.End, end.ToUnixTimeSeconds()) + .SetQueryParam(RequestParameters.Granularity, granularity.GetEnumMemberValue()) + .GetJsonAsync(); + + response.Data = candlesPage; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + + async Task> IProductsEndpoint.GetMarketTradesAsync(string productId, int limit) + { + var response = new ApiResponse(); + + try + { + if (string.IsNullOrWhiteSpace(productId)) throw new ArgumentNullException(nameof(productId), ErrorMessages.ProductIdRequired); + if (limit < 1 || limit > 250) throw new ArgumentException(ErrorMessages.LimitParameterRange, nameof(limit)); + + var tradesPage = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.ProductsEndpoint) + .AppendPathSegment(productId) + .AppendPathSegment(ApiEndpoints.TickerEndpoint) + .SetQueryParam(RequestParameters.Limit, limit) + .GetJsonAsync(); + + response.Data = tradesPage; + response.Success = true; + } + catch (Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/TransactionSummaryEndpoint.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/TransactionSummaryEndpoint.cs index 5f0e7c6..7688b1f 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/TransactionSummaryEndpoint.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Endpoints/TransactionSummaryEndpoint.cs @@ -1,45 +1,45 @@ -using CoinbaseAdvancedTradeClient.Constants; -using CoinbaseAdvancedTradeClient.Enums; -using CoinbaseAdvancedTradeClient.Extensions; -using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; -using CoinbaseAdvancedTradeClient.Models.Api.Common; -using CoinbaseAdvancedTradeClient.Models.Api.TransactionSummaries; -using CoinbaseAdvancedTradeClient.Resources; -using Flurl.Http; - -namespace CoinbaseAdvancedTradeClient -{ - public partial class CoinbaseAdvancedTradeApiClient : ITransactionSummaryEndpoint - { - public ITransactionSummaryEndpoint TransactionSummary => this; - - async Task> ITransactionSummaryEndpoint.GetTransactionSummaryAsync(DateTimeOffset startDate, DateTimeOffset endDate, string userNativeCurrency, ProductType productType) - { - var response = new ApiResponse(); - - try - { - if (startDate.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.StartDateRequired, nameof(startDate)); - if (endDate.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.EndDateRequired, nameof(endDate)); - - var transactionSummary = await _config.ApiUrl - .WithClient(this) - .AppendPathSegment(ApiEndpoints.TransactionSummaryEndpoint) - .SetQueryParam(RequestParameters.StartDate, startDate.ToUniversalTime()) - .SetQueryParam(RequestParameters.EndDate, endDate.ToUniversalTime()) - .SetQueryParam(RequestParameters.UserNativeCurrency, userNativeCurrency) - .SetQueryParam(RequestParameters.ProductType, productType.GetEnumMemberValue()) - .GetJsonAsync(); - - response.Data = transactionSummary; - response.Success = true; - } - catch(Exception ex) - { - await HandleExceptionResponseAsync(ex, response); - } - - return response; - } - } -} +using CoinbaseAdvancedTradeClient.Constants; +using CoinbaseAdvancedTradeClient.Enums; +using CoinbaseAdvancedTradeClient.Extensions; +using CoinbaseAdvancedTradeClient.Interfaces.Endpoints; +using CoinbaseAdvancedTradeClient.Models.Api.Common; +using CoinbaseAdvancedTradeClient.Models.Api.TransactionSummaries; +using CoinbaseAdvancedTradeClient.Resources; +using Flurl; +using Flurl.Http; + +namespace CoinbaseAdvancedTradeClient +{ + public partial class CoinbaseAdvancedTradeApiClient : ITransactionSummaryEndpoint + { + public ITransactionSummaryEndpoint TransactionSummary => this; + + async Task> ITransactionSummaryEndpoint.GetTransactionSummaryAsync(DateTimeOffset startDate, DateTimeOffset endDate, string userNativeCurrency, ProductType productType) + { + var response = new ApiResponse(); + + try + { + if (startDate.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.StartDateRequired, nameof(startDate)); + if (endDate.Equals(DateTimeOffset.MinValue)) throw new ArgumentException(ErrorMessages.EndDateRequired, nameof(endDate)); + + var transactionSummary = await _config.ApiUrl + .AppendPathSegment(ApiEndpoints.TransactionSummaryEndpoint) + .SetQueryParam(RequestParameters.StartDate, startDate.ToUniversalTime()) + .SetQueryParam(RequestParameters.EndDate, endDate.ToUniversalTime()) + .SetQueryParam(RequestParameters.UserNativeCurrency, userNativeCurrency) + .SetQueryParam(RequestParameters.ProductType, productType.GetEnumMemberValue()) + .GetJsonAsync(); + + response.Data = transactionSummary; + response.Success = true; + } + catch(Exception ex) + { + await HandleExceptionResponseAsync(ex, response); + } + + return response; + } + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyConfig.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyConfig.cs new file mode 100644 index 0000000..f9bb1c7 --- /dev/null +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyConfig.cs @@ -0,0 +1,11 @@ +using CoinbaseAdvancedTradeClient.Constants; + +namespace CoinbaseAdvancedTradeClient.Models.Config +{ + public class SecretApiKeyConfig + { + public string KeyName { get; set; } + public string KeySecret { get; set; } + public string ApiUrl { get; set; } = ApiEndpoints.ApiEndpointBase; + } +} \ No newline at end of file diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyWebSocketConfig.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyWebSocketConfig.cs new file mode 100644 index 0000000..82f1875 --- /dev/null +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/Config/SecretApiKeyWebSocketConfig.cs @@ -0,0 +1,11 @@ +using CoinbaseAdvancedTradeClient.Constants; + +namespace CoinbaseAdvancedTradeClient.Models.Config +{ + public class SecretApiKeyWebSocketConfig + { + public string KeyName { get; set; } + public string KeySecret { get; set; } + public string WebSocketUrl { get; set; } = ApiEndpoints.WebSocketEndpoint; + } +} \ No newline at end of file diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/WebSocket/SubscriptionMessage.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/WebSocket/SubscriptionMessage.cs index f2e2c61..a5df86f 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/WebSocket/SubscriptionMessage.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Models/WebSocket/SubscriptionMessage.cs @@ -1,26 +1,20 @@ -using CoinbaseAdvancedTradeClient.Enums; -using Newtonsoft.Json; - -namespace CoinbaseAdvancedTradeClient.Models.WebSocket -{ - public class SubscriptionMessage - { - [JsonProperty("type")] - public SubscriptionType Type { get; set; } - - [JsonProperty("channel")] - public string Channel { get; set; } - - [JsonProperty("product_ids")] - public List ProductIds { get; set; } - - [JsonProperty("api_key")] - public string ApiKey { get; set; } - - [JsonProperty("timestamp")] - public string Timestamp { get; set; } - - [JsonProperty("signature")] - public string Signature { get; set; } - } -} +using CoinbaseAdvancedTradeClient.Enums; +using Newtonsoft.Json; + +namespace CoinbaseAdvancedTradeClient.Models.WebSocket +{ + public class SubscriptionMessage + { + [JsonProperty("type")] + public SubscriptionType Type { get; set; } + + [JsonProperty("channel")] + public string Channel { get; set; } + + [JsonProperty("product_ids")] + public List ProductIds { get; set; } + + [JsonProperty("jwt")] + public string Jwt { get; set; } + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.Designer.cs b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.Designer.cs index 367ebf6..9250e8d 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.Designer.cs +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.Designer.cs @@ -1,279 +1,324 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace CoinbaseAdvancedTradeClient.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class ErrorMessages { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ErrorMessages() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CoinbaseAdvancedTradeClient.Resources.ErrorMessages", typeof(ErrorMessages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to An Account ID is required.. - /// - public static string AccountIdRequired { - get { - return ResourceManager.GetString("AccountIdRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The amount must be greater than zero.. - /// - public static string AmountParameterRange { - get { - return ResourceManager.GetString("AmountParameterRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An API Configuration is required.. - /// - public static string ApiConfigRequired { - get { - return ResourceManager.GetString("ApiConfigRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An API Key is required.. - /// - public static string ApiKeyRequired { - get { - return ResourceManager.GetString("ApiKeyRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An API Secret is required.. - /// - public static string ApiSecretRequired { - get { - return ResourceManager.GetString("ApiSecretRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid candle granularity.. - /// - public static string CandleGranularityInvalid { - get { - return ResourceManager.GetString("CandleGranularityInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A valid channel is required.. - /// - public static string ChannelRequired { - get { - return ResourceManager.GetString("ChannelRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An end date is required.. - /// - public static string EndDateRequired { - get { - return ResourceManager.GetString("EndDateRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The 'limit' parameter must be between 1 and 250.. - /// - public static string LimitParameterRange { - get { - return ResourceManager.GetString("LimitParameterRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The limit price must be greater than zero.. - /// - public static string LimitPriceParameterRange { - get { - return ResourceManager.GetString("LimitPriceParameterRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A message received callback is required. This is a method that will be invoked when a WebSocket message is received.. - /// - public static string MessageReceivedCallbackRequired { - get { - return ResourceManager.GetString("MessageReceivedCallbackRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The 'offset' parameter must be greater than zero.. - /// - public static string OffsetParameterRange { - get { - return ResourceManager.GetString("OffsetParameterRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid order configuration.. - /// - public static string OrderConfigurationInvalid { - get { - return ResourceManager.GetString("OrderConfigurationInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An Order ID is required.. - /// - public static string OrderIdRequired { - get { - return ResourceManager.GetString("OrderIdRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An order is required.. - /// - public static string OrderParametersRequired { - get { - return ResourceManager.GetString("OrderParametersRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid order placement source.. - /// - public static string OrderPlacementSourceInvalid { - get { - return ResourceManager.GetString("OrderPlacementSourceInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid order side.. - /// - public static string OrderSideInvalid { - get { - return ResourceManager.GetString("OrderSideInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid order type.. - /// - public static string OrderTypeInvalid { - get { - return ResourceManager.GetString("OrderTypeInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A Product ID is required.. - /// - public static string ProductIdRequired { - get { - return ResourceManager.GetString("ProductIdRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid product type.. - /// - public static string ProductTypeInvalid { - get { - return ResourceManager.GetString("ProductTypeInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Product type is required.. - /// - public static string ProductTypeRequired { - get { - return ResourceManager.GetString("ProductTypeRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A start date is required.. - /// - public static string StartDateRequired { - get { - return ResourceManager.GetString("StartDateRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The stop price must be greater than zero.. - /// - public static string StopPriceParameterRange { - get { - return ResourceManager.GetString("StopPriceParameterRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Web Socket must be connected.. - /// - public static string WebSocketMustBeConnected { - get { - return ResourceManager.GetString("WebSocketMustBeConnected", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CoinbaseAdvancedTradeClient.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ErrorMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ErrorMessages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CoinbaseAdvancedTradeClient.Resources.ErrorMessages", typeof(ErrorMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An Account ID is required.. + /// + public static string AccountIdRequired { + get { + return ResourceManager.GetString("AccountIdRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The amount must be greater than zero.. + /// + public static string AmountParameterRange { + get { + return ResourceManager.GetString("AmountParameterRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An API Configuration is required.. + /// + public static string ApiConfigRequired { + get { + return ResourceManager.GetString("ApiConfigRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An API Key is required.. + /// + public static string ApiKeyRequired { + get { + return ResourceManager.GetString("ApiKeyRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An API Secret is required.. + /// + public static string ApiSecretRequired { + get { + return ResourceManager.GetString("ApiSecretRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid candle granularity.. + /// + public static string CandleGranularityInvalid { + get { + return ResourceManager.GetString("CandleGranularityInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A valid channel is required.. + /// + public static string ChannelRequired { + get { + return ResourceManager.GetString("ChannelRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An end date is required.. + /// + public static string EndDateRequired { + get { + return ResourceManager.GetString("EndDateRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The 'limit' parameter must be between 1 and 250.. + /// + public static string LimitParameterRange { + get { + return ResourceManager.GetString("LimitParameterRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The limit price must be greater than zero.. + /// + public static string LimitPriceParameterRange { + get { + return ResourceManager.GetString("LimitPriceParameterRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A message received callback is required. This is a method that will be invoked when a WebSocket message is received.. + /// + public static string MessageReceivedCallbackRequired { + get { + return ResourceManager.GetString("MessageReceivedCallbackRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The 'offset' parameter must be greater than zero.. + /// + public static string OffsetParameterRange { + get { + return ResourceManager.GetString("OffsetParameterRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid order configuration.. + /// + public static string OrderConfigurationInvalid { + get { + return ResourceManager.GetString("OrderConfigurationInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An Order ID is required.. + /// + public static string OrderIdRequired { + get { + return ResourceManager.GetString("OrderIdRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An order is required.. + /// + public static string OrderParametersRequired { + get { + return ResourceManager.GetString("OrderParametersRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid order placement source.. + /// + public static string OrderPlacementSourceInvalid { + get { + return ResourceManager.GetString("OrderPlacementSourceInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid order side.. + /// + public static string OrderSideInvalid { + get { + return ResourceManager.GetString("OrderSideInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid order type.. + /// + public static string OrderTypeInvalid { + get { + return ResourceManager.GetString("OrderTypeInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A Product ID is required.. + /// + public static string ProductIdRequired { + get { + return ResourceManager.GetString("ProductIdRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid product type.. + /// + public static string ProductTypeInvalid { + get { + return ResourceManager.GetString("ProductTypeInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Product type is required.. + /// + public static string ProductTypeRequired { + get { + return ResourceManager.GetString("ProductTypeRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A start date is required.. + /// + public static string StartDateRequired { + get { + return ResourceManager.GetString("StartDateRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stop price must be greater than zero.. + /// + public static string StopPriceParameterRange { + get { + return ResourceManager.GetString("StopPriceParameterRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Web Socket must be connected.. + /// + public static string WebSocketMustBeConnected { + get { + return ResourceManager.GetString("WebSocketMustBeConnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A request method is required.. + /// + public static string RequestMethodRequired { + get { + return ResourceManager.GetString("RequestMethodRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A request host is required.. + /// + public static string RequestHostRequired { + get { + return ResourceManager.GetString("RequestHostRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A request path is required.. + /// + public static string RequestPathRequired { + get { + return ResourceManager.GetString("RequestPathRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid base64 key format.. + /// + public static string InvalidBase64KeyFormat { + get { + return ResourceManager.GetString("InvalidBase64KeyFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Ed25519 key length. Expected 64 bytes.. + /// + public static string InvalidEd25519KeyLength { + get { + return ResourceManager.GetString("InvalidEd25519KeyLength", resourceCulture); + } + } + } +} diff --git a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.resx b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.resx index ff49964..10856ab 100644 --- a/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.resx +++ b/CoinbaseAdvancedTradeClient/CoinbaseAdvancedTradeClient/Resources/ErrorMessages.resx @@ -1,192 +1,207 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An Account ID is required. - - - The amount must be greater than zero. - - - An API Configuration is required. - - - An API Key is required. - - - An API Secret is required. - - - Invalid candle granularity. - - - A valid channel is required. - - - An end date is required. - - - The 'limit' parameter must be between 1 and 250. - - - The limit price must be greater than zero. - - - A message received callback is required. This is a method that will be invoked when a WebSocket message is received. - - - The 'offset' parameter must be greater than zero. - - - Invalid order configuration. - - - An Order ID is required. - - - An order is required. - - - Invalid order placement source. - - - Invalid order side. - - - Invalid order type. - - - A Product ID is required. - - - Invalid product type. - - - Product type is required. - - - A start date is required. - - - The stop price must be greater than zero. - - - Web Socket must be connected. - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An Account ID is required. + + + The amount must be greater than zero. + + + An API Configuration is required. + + + An API Key is required. + + + An API Secret is required. + + + Invalid candle granularity. + + + A valid channel is required. + + + An end date is required. + + + The 'limit' parameter must be between 1 and 250. + + + The limit price must be greater than zero. + + + A message received callback is required. This is a method that will be invoked when a WebSocket message is received. + + + The 'offset' parameter must be greater than zero. + + + Invalid order configuration. + + + An Order ID is required. + + + An order is required. + + + Invalid order placement source. + + + Invalid order side. + + + Invalid order type. + + + A Product ID is required. + + + Invalid product type. + + + Product type is required. + + + A start date is required. + + + The stop price must be greater than zero. + + + Web Socket must be connected. + + + A request method is required. + + + A request host is required. + + + A request path is required. + + + Invalid base64 key format. + + + Invalid Ed25519 key length. Expected 64 bytes. + \ No newline at end of file