From 7c2b562b90e8510d2f117d88293af06e99a9159f Mon Sep 17 00:00:00 2001 From: Roman Yavnikov <45608740+Romazes@users.noreply.github.com> Date: Thu, 22 Aug 2024 00:16:24 +0300 Subject: [PATCH] Feature: new Authentication process (#23) * feat: add additional pckges to proj * feat: REST implement of JWT auth * test:fix: ApiTests * feat: implement of JWT for WebSocket connection * refactor: new Auth in ApiClient * test:refactor: CoinbaseBrokerage tests * rename: apiKey and apiSecret to name and privateKey * rename: config name of keys * remove: extra nuget to validate JWT token test:feat: validate JWT token * fix: missed OrderStatus in Order WS response * refactor: use aggregator instance like part of Instance * remove: extra GenerateRestToken in ApiClient * refactor: reuse constant variable to save performance * refactor: RandomHex() * remove: parameter in RandomHex * refactor: ParseKey() in ApiClient * feat: condition to prevent creating string WS response --- .github/workflows/gh-actions.yml | 4 +- .../CoinbaseApiTests.cs | 137 +++++++++++--- .../CoinbaseBrokerageAdditionalTests.cs | 7 +- .../CoinbaseBrokerageDataQueueHandlerTests.cs | 25 ++- .../CoinbaseBrokerageHistoryProviderTests.cs | 6 +- .../CoinbaseBrokerageTests.cs | 60 +++++-- ...uantConnect.CoinbaseBrokerage.Tests.csproj | 2 + .../config.json | 4 +- .../CoinbaseDownloader.cs | 6 +- .../CoinbaseExchangeInfoDownloader.cs | 6 +- .../Program.cs | 2 +- .../Api/CoinbaseApi.cs | 24 +-- .../Api/CoinbaseApiClient.cs | 170 +++++++++++++----- .../CoinbaseBrokerage.DataQueueHandler.cs | 8 +- .../CoinbaseBrokerage.Messaging.cs | 11 +- .../CoinbaseBrokerage.cs | 49 ++--- .../CoinbaseBrokerageFactory.cs | 14 +- .../Models/CoinbaseSubscriptionMessage.cs | 29 +-- .../Models/Enums/OrderStatus.cs | 7 + .../QuantConnect.CoinbaseBrokerage.csproj | 65 ++++--- 20 files changed, 417 insertions(+), 219 deletions(-) diff --git a/.github/workflows/gh-actions.yml b/.github/workflows/gh-actions.yml index ea095ba..64c8318 100644 --- a/.github/workflows/gh-actions.yml +++ b/.github/workflows/gh-actions.yml @@ -8,8 +8,8 @@ jobs: build: runs-on: ubuntu-20.04 env: - QC_COINBASE_API_SECRET: ${{ secrets.QC_COINBASE_API_SECRET }} - QC_COINBASE_API_KEY: ${{ secrets.QC_COINBASE_API_KEY }} + QC_COINBASE_API_NAME: ${{ secrets.QC_COINBASE_API_NAME }} + QC_COINBASE_API_PRIVATE_KEY: ${{ secrets.QC_COINBASE_API_PRIVATE_KEY }} QC_COINBASE_URL: ${{ secrets.QC_COINBASE_URL }} QC_COINBASE_REST_API: ${{ secrets.QC_COINBASE_REST_API }} QC_JOB_USER_ID: ${{ secrets.JOB_USER_ID }} diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs index a62a821..7814947 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs @@ -20,6 +20,9 @@ using Newtonsoft.Json.Linq; using QuantConnect.Configuration; using System.Collections.Generic; +using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using QuantConnect.Brokerages.Coinbase.Api; using QuantConnect.Brokerages.Coinbase.Models.Enums; using QuantConnect.Brokerages.Coinbase.Models.WebSocket; @@ -35,30 +38,38 @@ public class CoinbaseApiTests [SetUp] public void Setup() { - var apiKey = Config.Get("coinbase-api-key"); - var apiKeySecret = Config.Get("coinbase-api-secret"); + var name = Config.Get("coinbase-api-name"); + var priavteKey = Config.Get("coinbase-api-private-key"); - CoinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); + CoinbaseApi = CreateCoinbaseApi(name, priavteKey); } - [TestCase("", "")] - [TestCase("1", "2")] - public void InvalidAuthenticationCredentialsShouldThrowException(string apiKey, string apiKeySecret) + [TestCase("", "", typeof(ArgumentOutOfRangeException))] + [TestCase("organizations/2c7dhs-a3a3-4acf-aa0c-f68584f34c37/apiKeys/41090ffa-asd2-4040-815f-afaf63747e35", "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPcJGfXYEdLQi0iFj1xvGfPwuRNoeddwuKS4xL2NrlGWpoAoGCCqGSM49\nAwEHoUQDQgAEclN+asd/EhJ3UjOWkHmP/iqGBv5NkNJ75bUq\nVgxS4aU3/djHiIuSf27QasdOFIDGJLmOn7YiQ==\n-----END EC PRIVATE KEY-----\n", typeof(CryptographicException))] + [TestCase("organizations/2c7dhs-a3a3-4acf-aa0c-f68584f34c37/apiKeys/41090ffa-asd2-4040-815f-afaf63747e35", "MHcCAQEEIPcJGfXYEdLQi0iFj1xvGfPwuRNoeddwuKS4xL2NrlGWpoAoGCCqGSM49\nAwEHoUQDQgAEclN+asd/EhJ3UjOWkHmP/iqGBv5NkNJ75bUq\nVgxS4aU3/djHiIuSf27QasdOFIDGJLmOn7YiQ==", typeof(CryptographicException))] + public void InvalidAuthenticationCredentialsShouldThrowException(string name, string privateKey, Type expectedException) { - var coinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); + try + { + var coinbaseApi = CreateCoinbaseApi(name, privateKey); - // call random endpoint with incorrect credential - Assert.Throws(() => coinbaseApi.GetAccounts()); + // call random endpoint with incorrect credential + Assert.Throws(expectedException, () => coinbaseApi.GetAccounts()); + } + catch (Exception ex) + { + Assert.IsInstanceOf(expectedException, ex); + } } - [Test] - public void GetListAccounts() + [Test] + public void GetListAccounts() { var accounts = CoinbaseApi.GetAccounts(); Assert.Greater(accounts.Count(), 0); - foreach(var account in accounts) + foreach (var account in accounts) { Assert.IsTrue(account.Active); Assert.IsNotEmpty(account.Name); @@ -161,22 +172,22 @@ public void ParseWebSocketLevel2DataResponse() Assert.GreaterOrEqual(tick.NewQuantity, 0); Assert.GreaterOrEqual(tick.PriceLevel, 0); Assert.IsInstanceOf(tick.Side); - } + } } - [TestCase("/api/v3/brokerage/orders", null, "Unauthorized")] - [TestCase("/api/v3/brokerage/orders", "", "Unauthorized")] + [TestCase("/api/v3/brokerage/orders", null, "Bad Request")] + [TestCase("/api/v3/brokerage/orders", "", "Bad Request")] [TestCase("/api/v3/brokerage/orders", "{null}", "Bad Request")] - [TestCase("/api/v3/brokerage/orders", "[]", "Unauthorized")] + [TestCase("/api/v3/brokerage/orders", "[]", "Bad Request")] public void ValidateCoinbaseRestRequestWithWrongBodyParameter(string uriPath, object bodyData, string message) { - var apiKey = Config.Get("coinbase-api-key"); - var apiKeySecret = Config.Get("coinbase-api-secret"); + var name = Config.Get("coinbase-api-name"); + var privateKey = Config.Get("coinbase-api-private-key"); var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); var request = new RestRequest($"{uriPath}", Method.POST); - var _apiClient = new CoinbaseApiClient(apiKey, apiKeySecret, restApiUrl, 30); + var _apiClient = new CoinbaseApiClient(name, privateKey, restApiUrl, 30); request.AddJsonBody(bodyData); @@ -199,11 +210,95 @@ public void CancelOrderWithWrongOrderId(string orderId, string errorMessage) Assert.AreEqual(errorMessage, response.FailureReason); } - private CoinbaseApi CreateCoinbaseApi(string apiKey, string apiKeySecret) + [Test] + public void ValidateGenerateWebSocketJWTToken() + { + var name = Config.Get("coinbase-api-name"); + var privateKey = Config.Get("coinbase-api-private-key"); + var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); + + var _apiClient = new CoinbaseApiClient(name, privateKey, restApiUrl, 30); + + var generateWebSocketJWTToken = _apiClient.GenerateWebSocketToken(); + + var parsedPrivateKey = _apiClient.ParseKey(privateKey); + + Assert.IsTrue(IsTokenValid(generateWebSocketJWTToken, name, parsedPrivateKey)); + } + + /// + /// Validates a JWT token using ECDsa key with the specified token ID and secret. + /// + /// The JWT token to be validated. + /// The unique identifier for the ECDsa security key. + /// The ECDsa private key in Base64 format used to validate the token's signature. + /// + /// true if the token is successfully validated; otherwise, false. + /// + /// + /// This method is useful for verifying the authenticity of JWT tokens using ECDsa keys. + /// It ensures that the token's signature matches the expected signature derived from the provided secret. + /// + private bool IsTokenValid(string token, string tokenId, string parsedPrivateKey) + { + if (token == null) + return false; + + var key = ECDsa.Create(); + key?.ImportECPrivateKey(Convert.FromBase64String(parsedPrivateKey), out _); + + var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId }; + + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = securityKey, + ValidateIssuer = false, + ValidateAudience = false, + ClockSkew = TimeSpan.FromSeconds(100), + ValidateLifetime = true, + LifetimeValidator = CustomLifetimeValidator, + }, out var validatedToken); + + return true; + } + catch + { + return false; + } + } + + /// + /// Custom validator for checking the token's lifetime. + /// + /// The 'Not Before' date/time from the token's claims. + /// The expiration date/time from the token's claims. + /// The security token being validated. + /// The token validation parameters. + /// + /// true if the token is valid based on its expiration time; otherwise, false. + /// + /// + /// This custom lifetime validator ensures that the JWT token has not expired. It compares the + /// token's expiration time against the current UTC time to determine its validity. + /// + private static bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken tokenToValidate, TokenValidationParameters @param) + { + if (expires != null) + { + return expires > DateTime.UtcNow; + } + return false; + } + + private CoinbaseApi CreateCoinbaseApi(string name, string privateKey) { var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); - return new CoinbaseApi(new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase), null, apiKey, apiKeySecret, restApiUrl); + return new CoinbaseApi(new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase), null, name, privateKey, restApiUrl); } } } diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs index 7d32825..cf10e32 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs @@ -71,12 +71,11 @@ private static CoinbaseBrokerage GetBrokerage() { var wssUrl = Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com"); var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); - var apiKey = Config.Get("coinbase-api-key"); - var apiSecret = Config.Get("coinbase-api-secret"); + var name = Config.Get("coinbase-api-name"); + var privateKey = Config.Get("coinbase-api-private-key"); var algorithm = new QCAlgorithm(); - var aggregator = new AggregationManager(); - return new CoinbaseBrokerage(wssUrl, apiKey, apiSecret, restApiUrl, algorithm, aggregator, null); + return new CoinbaseBrokerage(wssUrl, name, privateKey, restApiUrl, algorithm, null); } } } diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs index 8458801..7322372 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs @@ -18,11 +18,12 @@ using NUnit.Framework; using System.Threading; using QuantConnect.Data; +using QuantConnect.Tests; using QuantConnect.Logging; using Microsoft.CodeAnalysis; using QuantConnect.Data.Market; using System.Collections.Generic; -using QuantConnect.Tests; +using System.Collections.Concurrent; namespace QuantConnect.Brokerages.Coinbase.Tests { @@ -46,7 +47,7 @@ private static IEnumerable TestParameters yield return new TestCaseData(BTCUSDC, Resolution.Second); yield return new TestCaseData(BTCUSDC, Resolution.Minute); yield return new TestCaseData(Symbol.Create("ETHUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Minute); - yield return new TestCaseData(Symbol.Create("GRTUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Second); + yield return new TestCaseData(Symbol.Create("SOLUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Second); } } @@ -193,7 +194,9 @@ public void SubscribeOnMultipleSymbols(List liquidSymbol { var cancelationToken = new CancellationTokenSource(); var startTime = DateTime.UtcNow; + var obj = new object(); var tickResetEvent = new ManualResetEvent(false); + var minimumAvailableReturnResponseData = liquidSymbolsSubscriptionConfigs.Count / 2; _brokerage.Message += (_, brokerageMessageEvent) => { @@ -202,7 +205,7 @@ public void SubscribeOnMultipleSymbols(List liquidSymbol cancelationToken.Cancel(); }; - var symbolTicks = new Dictionary(); + var symbolTicks = new ConcurrentDictionary(); foreach (var config in liquidSymbolsSubscriptionConfigs) { ProcessFeed(_brokerage.Subscribe(config, (s, e) => { }), @@ -218,14 +221,18 @@ public void SubscribeOnMultipleSymbols(List liquidSymbol Assert.IsTrue(tick.Price > 0, "Price was not greater then zero"); Assert.IsTrue(tick.Value > 0, "Value was not greater then zero"); - if (!symbolTicks.TryGetValue(tick.Symbol, out var symbol)) + lock (obj) { - symbolTicks[tick.Symbol] = true; - } + if (!symbolTicks.TryGetValue(tick.Symbol, out var symbol)) + { + _brokerage.Unsubscribe(config); + symbolTicks[tick.Symbol] = true; + } - if (symbolTicks.Count == liquidSymbolsSubscriptionConfigs.Count / 2) - { - tickResetEvent.Set(); + if (symbolTicks.Count == minimumAvailableReturnResponseData) + { + tickResetEvent.Set(); + } } } }); diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs index 3844154..0603558 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs @@ -24,7 +24,6 @@ using QuantConnect.Data.Market; using System.Collections.Generic; using QuantConnect.Configuration; -using QuantConnect.Lean.Engine.DataFeeds; namespace QuantConnect.Brokerages.Coinbase.Tests { @@ -36,11 +35,10 @@ public void GetsHistory(Symbol symbol, Resolution resolution, TickType tickType, { var brokerage = new CoinbaseBrokerage( Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com"), - Config.Get("coinbase-api-key"), - Config.Get("coinbase-api-secret"), + Config.Get("coinbase-api-name"), + Config.Get("coinbase-api-private-key"), Config.Get("coinbase-rest-api", "https://api.coinbase.com"), null, - new AggregationManager(), null); var now = DateTime.UtcNow; diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs index ef5da8a..295a7d5 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs @@ -17,9 +17,11 @@ using System; using NUnit.Framework; using System.Threading; +using QuantConnect.Data; using QuantConnect.Orders; using QuantConnect.Interfaces; using QuantConnect.Securities; +using QuantConnect.Data.Market; using System.Collections.Generic; using QuantConnect.Configuration; using QuantConnect.Tests.Brokerages; @@ -33,6 +35,19 @@ namespace QuantConnect.Brokerages.Coinbase.Tests public partial class CoinbaseBrokerageTests : BrokerageTests { #region Properties + + /// + /// The currency used as the quote currency in the trading pair. + /// + /// + /// This field represents the quote currency for the trading pair, which is combined with the base currency + /// to form the full symbol. For example, in the symbol which represents "BTCUSDC", + /// "BTC" is the base currency, and "USDC" (stored in this field) is the quote currency. + /// The quote currency is what the base currency is being traded against, and in this case, "USDC" is + /// the stablecoin used on the Coinbase platform. + /// + private string QuoteCurrency = "USDC"; + protected override Symbol Symbol => Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase); protected virtual ISymbolMapper SymbolMapper => new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase); @@ -52,29 +67,29 @@ protected override decimal GetDefaultQuantity() protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider) { - var securities = new SecurityManager(new TimeKeeper(DateTime.UtcNow, TimeZones.NewYork)) + var securityManager = new SecurityManager(new TimeKeeper(DateTime.UtcNow, TimeZones.NewYork)) { - {Symbol, CreateSecurity(Symbol)} - }; + CreateSecurity(Symbol) + }; - var transactions = new SecurityTransactionManager(null, securities); + var transactions = new SecurityTransactionManager(null, securityManager); transactions.SetOrderProcessor(new FakeOrderProcessor()); var algorithmSettings = new AlgorithmSettings(); var algorithm = new Mock(); algorithm.Setup(a => a.Transactions).Returns(transactions); algorithm.Setup(a => a.BrokerageModel).Returns(new CoinbaseBrokerageModel()); - algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securities, transactions, algorithmSettings)); - algorithm.Setup(a => a.Securities).Returns(securities); + algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securityManager, transactions, algorithmSettings)); + algorithm.Setup(a => a.Securities).Returns(securityManager); - var apiKey = Config.Get("coinbase-api-key"); - var apiSecret = Config.Get("coinbase-api-secret"); + var name = Config.Get("coinbase-api-name"); + var privateKey = Config.Get("coinbase-api-private-key"); var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); var webSocketUrl = Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com"); - _api = new CoinbaseApi(SymbolMapper, null, apiKey, apiSecret, restApiUrl); + _api = new CoinbaseApi(SymbolMapper, null, name, privateKey, restApiUrl); - return new CoinbaseBrokerage(webSocketUrl, apiKey, apiSecret, restApiUrl, algorithm.Object, orderProvider, new AggregationManager(), null); + return new CoinbaseBrokerage(webSocketUrl, name, privateKey, restApiUrl, algorithm.Object, orderProvider, null); } /// @@ -252,5 +267,30 @@ public void UpdateOrderWithWrongParameters(OrderTestParameters orderTestParam) Assert.Throws(() => Brokerage.UpdateOrder(order)); } + + private Security CreateSecurity(Symbol symbol) + { + var timezone = TimeZones.NewYork; + + var config = new SubscriptionDataConfig( + typeof(TradeBar), + symbol, + Resolution.Hour, + timezone, + timezone, + true, + false, + false); + + return new Security( + SecurityExchangeHours.AlwaysOpen(timezone), + config, + new Cash(QuoteCurrency, 0, 1), + new SymbolProperties(symbol.Value, QuoteCurrency, 1, 0.01m, 0.00000001m, string.Empty), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache() + ); + } } } diff --git a/QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj b/QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj index 4acd9fc..db381e3 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj +++ b/QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj @@ -17,6 +17,7 @@ + @@ -24,6 +25,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/QuantConnect.CoinbaseBrokerage.Tests/config.json b/QuantConnect.CoinbaseBrokerage.Tests/config.json index 6906dd3..6aa938f 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/config.json +++ b/QuantConnect.CoinbaseBrokerage.Tests/config.json @@ -2,6 +2,6 @@ "data-folder": "../../../../Lean/Data/", "coinbase-rest-api": "https://api.coinbase.com", "coinbase-url": "wss://advanced-trade-ws.coinbase.com", - "coinbase-api-key": "", - "coinbase-api-secret": "" + "coinbase-api-name": "", + "coinbase-api-private-key": "" } \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs index cabbefb..dad8b45 100644 --- a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs @@ -92,10 +92,10 @@ public IEnumerable Get(DataDownloaderGetParameters dataDownloaderGetPa /// private Brokerage CreateBrokerage() { - var apiKey = Config.Get("coinbase-api-key", ""); - var apiSecret = Config.Get("coinbase-api-secret", ""); + var name = Config.Get("coinbase-api-name", ""); + var privateKey = Config.Get("coinbase-api-private-key", ""); var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); - return new CoinbaseBrokerage(string.Empty, apiKey, apiSecret, restApiUrl, null, null, null); + return new CoinbaseBrokerage(string.Empty, name, privateKey, restApiUrl, null, null, null); } } } diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs index 3174abd..e797e4d 100644 --- a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs @@ -66,10 +66,10 @@ public IEnumerable Get() /// private CoinbaseApi CreateCoinbaseApi() { - var apiKey = Config.Get("coinbase-api-key"); - var apiSecret = Config.Get("coinbase-api-secret"); + var name = Config.Get("coinbase-api-name"); + var privateKey = Config.Get("coinbase-api-private-key"); var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); - return new CoinbaseApi(null, null, apiKey, apiSecret, restApiUrl); + return new CoinbaseApi(null, null, name, privateKey, restApiUrl); } } } diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs index ea9c7ea..fc98c68 100644 --- a/QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs @@ -34,7 +34,7 @@ private static void Main(string[] args) PrintMessageAndExit(1, "ERROR: --app value is required"); } - if(string.IsNullOrEmpty(Config.GetValue("coinbase-api-key")) || string.IsNullOrEmpty(Config.GetValue("coinbase-api-secret"))) + if(string.IsNullOrEmpty(Config.GetValue("coinbase-api-name")) || string.IsNullOrEmpty(Config.GetValue("coinbase-api-private-key"))) { PrintMessageAndExit(1, "ERROR: check configs: 'coinbase-api-key' or 'coinbase-api-secret'"); } diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs index 0654acd..aa86f45 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs @@ -68,34 +68,22 @@ public class CoinbaseApi : IDisposable private ISecurityProvider SecurityProvider { get; } public CoinbaseApi(ISymbolMapper symbolMapper, ISecurityProvider securityProvider, - string apiKey, string apiKeySecret, string restApiUrl) + string name, string privateKey, string restApiUrl) { SymbolMapper = symbolMapper; SecurityProvider = securityProvider; - _apiClient = new CoinbaseApiClient(apiKey, apiKeySecret, restApiUrl, maxGateLimitOccurrences); + _apiClient = new CoinbaseApiClient(name, privateKey, restApiUrl, maxGateLimitOccurrences); } /// - /// Generates WebSocket signatures for authentication. + /// Retrieves a JWT token for authenticating WebSocket connections. /// - /// The WebSocket channel for which the signature is generated. - /// A collection of product identifiers for which the signature is generated. /// - /// A tuple containing the API key, timestamp, and signature required for WebSocket authentication. + /// A representing the JWT token used for WebSocket authentication. /// - /// - /// The parameter specifies the WebSocket channel, - /// and contains a collection of product identifiers for which the authentication signature is generated. - /// - /// - /// This example demonstrates how to use the GetWebSocketSignatures method: - /// - /// var (apiKey, timestamp, signature) = GetWebSocketSignatures("trades", new List { "BTC-USD", "ETH-USD" }); - /// - /// - public (string apiKey, string timestamp, string signature) GetWebSocketSignatures(string channel, ICollection productIds) + public string GetWebSocketJWTToken() { - return _apiClient.GenerateWebSocketSignature(channel, productIds); + return _apiClient.GenerateWebSocketToken(); } /// diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 7a045e9..19f89d5 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -13,16 +13,18 @@ * limitations under the License. */ +using Jose; using System; using RestSharp; using System.Net; -using System.Text; using System.Linq; using QuantConnect.Util; using System.Diagnostics; -using System.Globalization; using System.Collections.Generic; using System.Security.Cryptography; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("QuantConnect.Brokerages.Coinbase.Tests")] namespace QuantConnect.Brokerages.Coinbase.Api; @@ -31,17 +33,64 @@ namespace QuantConnect.Brokerages.Coinbase.Api; /// public class CoinbaseApiClient : IDisposable { - private readonly string _apiKey; - private readonly HMACSHA256 _hmacSha256; + /// + /// Provides a thread-safe random number generator instance. + /// + private readonly static Random _random = new Random(); + + /// + /// Stores the CDP API key name used for authenticating requests. + /// + /// + /// This field holds the API key name, which is essential for identifying and authenticating + /// API requests made to Coinbase's services. The key is provided during the initialization + /// of the . + /// + private readonly string _name; + + /// + /// Represents an ECDSA private key used for cryptographic operations. + /// The private key is initialized from a base64-encoded string and imported into an ECDSA instance. + /// + private readonly ECDsa _privateKey; + + /// + /// Represents the REST client used to send HTTP requests to the Coinbase API. + /// private readonly RestClient _restClient; + + /// + /// Manages rate limiting for outbound API requests. + /// private readonly RateGate _rateGate; - public CoinbaseApiClient(string apiKey, string apiKeySecret, string restApiUrl, int maxRequestsPerSecond) + /// + /// Represents the Unix epoch time, which is the starting point for Unix time calculation. + /// + private static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Initializes a new instance of the class with the specified CDP API keys, + /// REST API URL, and maximum requests per second. + /// + /// The CDP API key required for authenticating requests. + /// The CDP API key secret used to sign requests. This will be parsed into a usable format. + /// The base URL of the Coinbase REST API. + /// The maximum number of requests that can be sent to the API per second. + /// + /// This constructor sets up the Coinbase API client by initializing the CDP API key, parsing the private key, + /// configuring the REST client with the provided API URL, and setting up a rate limiter to ensure that + /// requests do not exceed the specified maximum rate. The helps prevent the client + /// from hitting rate limits imposed by the API. + /// + public CoinbaseApiClient(string name, string privateKey, string restApiUrl, int maxRequestsPerSecond) { - _apiKey = apiKey; + _name = name; _restClient = new RestClient(restApiUrl); - _hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(apiKeySecret)); _rateGate = new RateGate(maxRequestsPerSecond, Time.OneSecond); + + _privateKey = ECDsa.Create(); + _privateKey.ImportECPrivateKey(Convert.FromBase64String(ParseKey(privateKey)), out _); } /// @@ -54,17 +103,9 @@ public CoinbaseApiClient(string apiKey, string apiKeySecret, string restApiUrl, /// private void AuthenticateRequest(IRestRequest request) { - var body = request.Parameters.SingleOrDefault(b => b.Type == ParameterType.RequestBody); - - var urlPath = _restClient.BuildUri(request).AbsolutePath; - - var timestamp = GetNonce(); - - var signature = GetSign(timestamp, request.Method.ToString(), urlPath, body?.Value?.ToString() ?? string.Empty); - - request.AddHeader("CB-ACCESS-KEY", _apiKey); - request.AddHeader("CB-ACCESS-SIGN", signature); - request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp); + var uri = _restClient.BuildUri(request); + var generatedJWTToken = GenerateToken($"{request.Method} {uri.Host + uri.AbsolutePath}"); + request.AddOrUpdateHeader("Authorization", "Bearer " + generatedJWTToken); } /// @@ -96,50 +137,88 @@ public IRestResponse ExecuteRequest(IRestRequest request) } /// - /// Generates a signature for a given set of parameters using HMAC-SHA256. + /// Generates a JWT token for WebSocket connections. /// - /// The timestamp of the request. - /// The HTTP method used for the request (e.g., GET, POST). - /// The URL path of the request. - /// The request body. - /// - /// A string representation of the generated signature in lowercase hexadecimal format. - /// + /// A signed JWT token as a string. + public string GenerateWebSocketToken() + { + return GenerateToken(); + } + + /// + /// Generates a JWT token with the specified parameters using ECDsa signing. + /// + /// The URI to include in the token payload. Pass null for WebSocket tokens. + /// A signed JWT token as a string. /// - /// The signature is computed using the HMAC-SHA256 algorithm and is typically used for authentication and message integrity. + /// This method creates a JWT token with a subject, issuer, and a short expiration time, signed using the ES256 algorithm. + /// It also includes a nonce in the token headers to prevent replay attacks. /// - private string GetSign(string timeStamp, string httpMethod, string urlPath, string body) + private string GenerateToken(string uri = null) { - var preHash = timeStamp + httpMethod + urlPath + body; + var utcNow = DateTime.UtcNow; - var sig = _hmacSha256.ComputeHash(Encoding.UTF8.GetBytes(preHash)); + var payload = new Dictionary + { + { "sub", _name }, + { "iss", "coinbase-cloud" }, + { "nbf", Convert.ToInt64((utcNow - EpochTime).TotalSeconds) }, + { "exp", Convert.ToInt64((utcNow.AddMinutes(1) - EpochTime).TotalSeconds) } + }; - return Convert.ToHexString(sig).ToLowerInvariant(); + if (uri != null) + { + payload.Add("uri", uri); + } + + var extraHeaders = new Dictionary + { + { "kid", _name }, + // add nonce to prevent replay attacks with a random 10 digit number + { "nonce", RandomHex() }, + { "typ", "JWT"} + }; + + var encodedToken = JWT.Encode(payload, _privateKey, JwsAlgorithm.ES256, extraHeaders); + + return encodedToken; } - public (string apiKey, string timestamp, string signature) GenerateWebSocketSignature(string channel, ICollection productIds) + /// + /// Parses a key string by removing the first and last lines and returning the remaining content as a single string. + /// + /// The key string to be parsed. It is expected to have multiple lines, with each line separated by a newline character. + /// A string that concatenates the remaining lines after the first and last lines are removed. + /// + /// This method is useful when handling key formats that typically have headers and footers (e.g., PEM format). + /// It removes the first and last lines, which might contain non-essential information like "BEGIN" and "END" markers, + /// and returns the core content of the key. + /// + internal string ParseKey(string key) { - var timestamp = GetNonce(); - - var products = string.Join(",", productIds ?? Array.Empty()); + var keyLines = key.Split('\n', StringSplitOptions.RemoveEmptyEntries).ToList(); - var signature = GetSign(timestamp, string.Empty, channel, products); + // Check if the first and last lines are the BEGIN/END markers, and remove them if present + if (keyLines.First().Contains("BEGIN") && keyLines.Last().Contains("END")) + { + keyLines.RemoveAt(0); // Remove the first line + keyLines.RemoveAt(keyLines.Count - 1); // Remove the last line + } - return (_apiKey, timestamp, signature); + return string.Join("", keyLines); } /// - /// Generates a unique nonce based on the current UTC time in Unix timestamp format. + /// Generates a random hexadecimal string of a fixed length. /// /// - /// A string representation of the generated nonce. + /// A representing a random hexadecimal value of 10 characters. /// - /// - /// The nonce is used to ensure the uniqueness of each request, typically in the context of security and authentication. - /// - private static string GetNonce() + private static string RandomHex() { - return Time.DateTimeToUnixTimeStamp(DateTime.UtcNow).ToString("F0", CultureInfo.InvariantCulture); + byte[] buffer = new byte[10 / 2]; + _random.NextBytes(buffer); + return string.Concat(buffer.Select(x => x.ToString("X2")).ToArray()); } /// @@ -150,6 +229,7 @@ private static string GetNonce() /// public void Dispose() { - _hmacSha256.DisposeSafely(); + _rateGate.Dispose(); + _privateKey.Dispose(); } } diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs index 6877a44..d2432fa 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs @@ -71,17 +71,13 @@ public void Unsubscribe(SubscriptionDataConfig dataConfig) /// Job we're subscribing for public void SetJob(LiveNodePacket job) { - var aggregator = Composer.Instance.GetExportedValueByTypeName( - Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"), forceTypeNameOnExisting: false); - Initialize( webSocketUrl: job.BrokerageData["coinbase-url"], - apiKey: job.BrokerageData["coinbase-api-key"], - apiSecret: job.BrokerageData["coinbase-api-secret"], + name: job.BrokerageData["coinbase-api-name"], + privateKey: job.BrokerageData["coinbase-api-private-key"], restApiUrl: job.BrokerageData["coinbase-rest-api"], algorithm: null, orderProvider: null, - aggregator: aggregator, job: job ); diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs index 86ee28f..bb5ec8b 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs @@ -100,7 +100,10 @@ protected override void OnMessage(object _, WebSocketMessage webSocketMessage) { var data = webSocketMessage.Data as WebSocketClientWrapper.TextMessage; - Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(OnMessage)}: {data.Message}"); + if (Log.DebuggingEnabled) + { + Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(OnMessage)}: {data.Message}"); + } try { @@ -347,7 +350,7 @@ private void EmitTradeTick(CoinbaseMarketTradesEvent tradeUpdates) { continue; } - _tradeIds[symbol] = new (trade.TradeId, trade.Time.UtcDateTime); + _tradeIds[symbol] = new(trade.TradeId, trade.Time.UtcDateTime); var tick = new Tick { @@ -507,10 +510,10 @@ private void ManageChannelSubscription(WebSocketSubscriptionType subscriptionTyp throw new InvalidOperationException($"{nameof(CoinbaseBrokerage)}.{nameof(ManageChannelSubscription)}: WebSocketMustBeConnected"); } - var (apiKey, timestamp, signature) = _coinbaseApi.GetWebSocketSignatures(channel, productIds); + var jwtToken = _coinbaseApi.GetWebSocketJWTToken(); var json = JsonConvert.SerializeObject( - new CoinbaseSubscriptionMessage(apiKey, channel, productIds, signature, timestamp, subscriptionType)); + new CoinbaseSubscriptionMessage(channel, productIds, jwtToken, subscriptionType)); Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(ManageChannelSubscription)}:send json message: " + json); diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs index b1e035c..aadd4cb 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs @@ -31,6 +31,7 @@ using QuantConnect.Interfaces; using QuantConnect.Orders.Fees; using System.Collections.Generic; +using QuantConnect.Configuration; using System.Security.Cryptography; using System.Net.NetworkInformation; using QuantConnect.Brokerages.Coinbase.Api; @@ -89,15 +90,14 @@ public CoinbaseBrokerage() : base(MarketName) /// Initializes a new instance of the class with set of parameters. /// /// WebSockets url - /// api key - /// api secret + /// The CDP API key required for authenticating requests. + /// The CDP API key secret used to sign requests. This will be parsed into a usable format. /// api url /// the algorithm instance is required to retrieve account type - /// consolidate ticks /// The live job packet - public CoinbaseBrokerage(string webSocketUrl, string apiKey, string apiSecret, string restApiUrl, - IAlgorithm algorithm, IDataAggregator aggregator, LiveNodePacket job) - : this(webSocketUrl, apiKey, apiSecret, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, aggregator, job) + public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, string restApiUrl, + IAlgorithm algorithm, LiveNodePacket job) + : this(webSocketUrl, name, privateKey, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, job) { } @@ -106,25 +106,23 @@ public CoinbaseBrokerage(string webSocketUrl, string apiKey, string apiSecret, s /// Initializes a new instance of the class with set of parameters. /// /// WebSockets url - /// Api key - /// Api secret + /// The CDP API key required for authenticating requests. + /// The CDP API key secret used to sign requests. This will be parsed into a usable format. /// Api url /// The algorithm instance is required to retrieve account type /// The order provider - /// Consolidate ticks /// The live job packet - public CoinbaseBrokerage(string webSocketUrl, string apiKey, string apiSecret, string restApiUrl, - IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) + public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, string restApiUrl, + IAlgorithm algorithm, IOrderProvider orderProvider, LiveNodePacket job) : base(MarketName) { Initialize( webSocketUrl: webSocketUrl, - apiKey: apiKey, - apiSecret: apiSecret, + name: name, + privateKey: privateKey, restApiUrl: restApiUrl, algorithm: algorithm, orderProvider: orderProvider, - aggregator: aggregator, job: job ); } @@ -133,14 +131,13 @@ public CoinbaseBrokerage(string webSocketUrl, string apiKey, string apiSecret, s /// Initialize the instance of this class /// /// The web socket base url - /// api key - /// api secret + /// The CDP API key required for authenticating requests. + /// The CDP API key secret used to sign requests. This will be parsed into a usable format. /// the algorithm instance is required to retrieve account type /// The order provider - /// the aggregator for consolidating ticks /// The live job packet - protected void Initialize(string webSocketUrl, string apiKey, string apiSecret, string restApiUrl, - IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) + protected void Initialize(string webSocketUrl, string name, string privateKey, string restApiUrl, + IAlgorithm algorithm, IOrderProvider orderProvider, LiveNodePacket job) { if (IsInitialized) { @@ -149,13 +146,21 @@ protected void Initialize(string webSocketUrl, string apiKey, string apiSecret, ValidateSubscription(); - Initialize(webSocketUrl, new WebSocketClientWrapper(), null, apiKey, apiSecret); + Initialize(webSocketUrl, new WebSocketClientWrapper(), null, name, privateKey); _job = job; _algorithm = algorithm; - _aggregator = aggregator; + + _aggregator = Composer.Instance.GetPart(); + if (_aggregator == null) + { + // toolbox downloader case + var aggregatorName = Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"); + Log.Trace($"CoinbaseBrokerage.Initialize(): found no data aggregator instance, creating {aggregatorName}"); + _aggregator = Composer.Instance.GetExportedValueByTypeName(aggregatorName); + } _symbolMapper = new SymbolPropertiesDatabaseSymbolMapper(MarketName); - _coinbaseApi = new CoinbaseApi(_symbolMapper, algorithm?.Portfolio, apiKey, apiSecret, restApiUrl); + _coinbaseApi = new CoinbaseApi(_symbolMapper, algorithm?.Portfolio, name, privateKey, restApiUrl); OrderProvider = orderProvider; SubscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager() diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs index 38d5307..6ffd751 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs @@ -37,8 +37,8 @@ public class CoinbaseBrokerageFactory : BrokerageFactory /// public override Dictionary BrokerageData => new Dictionary { - { "coinbase-api-key", Config.Get("coinbase-api-key")}, - { "coinbase-api-secret", Config.Get("coinbase-api-secret")}, + { "coinbase-api-name", Config.Get("coinbase-api-name")}, + { "coinbase-api-private-key", Config.Get("coinbase-api-private-key")}, // Represents the configuration setting for the Coinbase API URL. { "coinbase-rest-api", Config.Get("coinbase-rest-api", "https://api.coinbase.com")}, // Represents the configuration setting for the Coinbase WebSocket URL. @@ -69,8 +69,8 @@ public CoinbaseBrokerageFactory() : base(typeof(CoinbaseBrokerage)) public override IBrokerage CreateBrokerage(Packets.LiveNodePacket job, IAlgorithm algorithm) { var errors = new List(); - var apiKey = Read(job.BrokerageData, "coinbase-api-key", errors); - var apiSecret = Read(job.BrokerageData, "coinbase-api-secret", errors); + var name = Read(job.BrokerageData, "coinbase-api-name", errors); + var privateKey = Read(job.BrokerageData, "coinbase-api-private-key", errors); var apiUrl = Read(job.BrokerageData, "coinbase-rest-api", errors); var wsUrl = Read(job.BrokerageData, "coinbase-url", errors); @@ -80,11 +80,7 @@ public override IBrokerage CreateBrokerage(Packets.LiveNodePacket job, IAlgorith throw new ArgumentException(string.Join(Environment.NewLine, errors)); } - var aggregator = Composer.Instance.GetExportedValueByTypeName( - Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"), - forceTypeNameOnExisting: false); - - var brokerage = new CoinbaseBrokerage(wsUrl, apiKey, apiSecret, apiUrl, algorithm, aggregator, job); + var brokerage = new CoinbaseBrokerage(wsUrl, name, privateKey, apiUrl, algorithm, job); // Add the brokerage to the composer to ensure its accessible to the live data feed. Composer.Instance.AddPart(brokerage); diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs index 456e579..bb1148b 100644 --- a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs @@ -25,10 +25,10 @@ namespace QuantConnect.Brokerages.Coinbase.Models; public readonly struct CoinbaseSubscriptionMessage { /// - /// Gets the API key for authentication (if required). + /// Gets the JWT for authentication. /// - [JsonProperty("api_key")] - public string ApiKey { get; } + [JsonProperty("jwt")] + public string JWT { get; } /// /// Gets the channel to subscribe to. @@ -42,18 +42,6 @@ public readonly struct CoinbaseSubscriptionMessage [JsonProperty("product_ids")] public List ProductIds { get; } - /// - /// Gets the signature for authentication (if required). - /// - [JsonProperty("signature")] - public string Signature { get; } - - /// - /// Gets the timestamp of the subscription message. - /// - [JsonProperty("timestamp")] - public string Timestamp { get; } - /// /// Gets the type of WebSocket subscription. /// @@ -63,21 +51,16 @@ public readonly struct CoinbaseSubscriptionMessage /// /// Initializes a new instance of the struct. /// - /// The API key for authentication (if required). /// The channel to subscribe to. /// The list of product IDs associated with the subscription. - /// The signature for authentication (if required). - /// The timestamp of the subscription message. + /// The generated JWT token for authentication. /// The type of WebSocket subscription. [JsonConstructor] - public CoinbaseSubscriptionMessage(string apiKey, string channel, List productIds, - string signature, string timestamp, WebSocketSubscriptionType type) + public CoinbaseSubscriptionMessage(string channel, List productIds, string jwtToken, WebSocketSubscriptionType type) { - ApiKey = apiKey; + JWT = jwtToken; Channel = channel; ProductIds = productIds; - Signature = signature; - Timestamp = timestamp; Type = type; } } diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs index 086eda0..3c08487 100644 --- a/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs @@ -66,4 +66,11 @@ public enum OrderStatus /// [EnumMember(Value = "FAILED")] Failed, + + /// + /// The order has been marked for cancellation, but the cancellation process has not yet completed. + /// This status indicates that the cancellation request is in a queue and will be processed shortly. + /// + [EnumMember(Value = "CANCEL_QUEUED")] + CancelQueued, } diff --git a/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj index 501e5f0..0e5275f 100644 --- a/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj +++ b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj @@ -1,34 +1,33 @@ - - - Release - AnyCPU - net6.0 - QuantConnect.Brokerages.Coinbase - QuantConnect.Brokerages.Coinbase - QuantConnect.Brokerages.Coinbase - QuantConnect.Brokerages.Coinbase - Library - bin\$(Configuration)\ - false - true - false - Coinbase Brokerage Integration to LEAN - - - full - bin\Debug\ - - - pdbonly - bin\Release\ - - - - - - - - - - + + + Release + AnyCPU + net6.0 + QuantConnect.Brokerages.Coinbase + QuantConnect.Brokerages.Coinbase + QuantConnect.Brokerages.Coinbase + QuantConnect.Brokerages.Coinbase + Library + bin\$(Configuration)\ + false + true + false + Coinbase Brokerage Integration to LEAN + + + full + bin\Debug\ + + + pdbonly + bin\Release\ + + + + + + + + + \ No newline at end of file