From f10ce79d10dc43943a41da6c41dec8953d539382 Mon Sep 17 00:00:00 2001 From: Romazes Date: Mon, 19 Aug 2024 17:36:05 +0300 Subject: [PATCH 01/17] feat: add additional pckges to proj --- .../QuantConnect.CoinbaseBrokerage.csproj | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj index 501e5f0..076ac1d 100644 --- a/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj +++ b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj @@ -1,34 +1,38 @@ - - - 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 From 3f4548cde15a11795401825ae867c87653a1e7b7 Mon Sep 17 00:00:00 2001 From: Romazes Date: Mon, 19 Aug 2024 17:44:57 +0300 Subject: [PATCH 02/17] feat: REST implement of JWT auth --- .../Api/CoinbaseApiClient.cs | 181 +++++++++++++++++- 1 file changed, 172 insertions(+), 9 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 7a045e9..368c9bd 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using Jose; using System; using RestSharp; using System.Net; @@ -23,6 +24,8 @@ using System.Globalization; using System.Collections.Generic; using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; namespace QuantConnect.Brokerages.Coinbase.Api; @@ -31,7 +34,9 @@ namespace QuantConnect.Brokerages.Coinbase.Api; /// public class CoinbaseApiClient : IDisposable { + private readonly static Random _random = new Random(); private readonly string _apiKey; + private readonly string _parsedCbPrivateKey; private readonly HMACSHA256 _hmacSha256; private readonly RestClient _restClient; private readonly RateGate _rateGate; @@ -40,6 +45,7 @@ public CoinbaseApiClient(string apiKey, string apiKeySecret, string restApiUrl, { _apiKey = apiKey; _restClient = new RestClient(restApiUrl); + _parsedCbPrivateKey = ParseKey(apiKeySecret); _hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(apiKeySecret)); _rateGate = new RateGate(maxRequestsPerSecond, Time.OneSecond); } @@ -54,17 +60,15 @@ public CoinbaseApiClient(string apiKey, string apiKeySecret, string restApiUrl, /// private void AuthenticateRequest(IRestRequest request) { - var body = request.Parameters.SingleOrDefault(b => b.Type == ParameterType.RequestBody); + var uri = _restClient.BuildUri(request); + var generatedJWTToken = GenerateRestToken(_apiKey, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); - var urlPath = _restClient.BuildUri(request).AbsolutePath; - - var timestamp = GetNonce(); - - var signature = GetSign(timestamp, request.Method.ToString(), urlPath, body?.Value?.ToString() ?? string.Empty); + if (!IsTokenValid(generatedJWTToken, _apiKey, _parsedCbPrivateKey)) + { + throw new InvalidOperationException("The generated JWT token is invalid. Authentication failed."); + } - request.AddHeader("CB-ACCESS-KEY", _apiKey); - request.AddHeader("CB-ACCESS-SIGN", signature); - request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp); + request.AddOrUpdateHeader("Authorization", "Bearer " + generatedJWTToken); } /// @@ -142,6 +146,165 @@ private static string GetNonce() return Time.DateTimeToUnixTimeStamp(DateTime.UtcNow).ToString("F0", CultureInfo.InvariantCulture); } + private static string GenerateRestToken(string name, string secret, string uri) + { + var privateKeyBytes = Convert.FromBase64String(secret); // Assuming PEM is base64 encoded + using var key = ECDsa.Create(); + key.ImportECPrivateKey(privateKeyBytes, out _); + + var payload = new Dictionary + { + { "sub", name }, + { "iss", "coinbase-cloud" }, + { "nbf", Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, + { "exp", Convert.ToInt64((DateTime.UtcNow.AddMinutes(1) - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, + { "uri", uri } + }; + + var extraHeaders = new Dictionary + { + { "kid", name }, + // add nonce to prevent replay attacks with a random 10 digit number + { "nonce", RandomHex(10) }, + { "typ", "JWT"} + }; + + var encodedToken = JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders); + + return encodedToken; + } + + /// + /// Generates a JWT token with the specified name and secret using ECDsa signing. + /// + /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. + /// The ECDsa private key in Base64 format used to sign the token. + /// A signed JWT token as a string. + /// + /// 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 static string GenerateWebSocketToken(string name, string secret) + { + var privateKeyBytes = Convert.FromBase64String(secret); // Assuming PEM is base64 encoded + using var key = ECDsa.Create(); + key.ImportECPrivateKey(privateKeyBytes, out _); + + var payload = new Dictionary + { + { "sub", name }, + { "iss", "coinbase-cloud" }, + { "nbf", Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, + { "exp", Convert.ToInt64((DateTime.UtcNow.AddMinutes(1) - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, + }; + + var extraHeaders = new Dictionary + { + { "kid", name }, + // add nonce to prevent replay attacks with a random 10 digit number + { "nonce", RandomHex(10) }, + { "typ", "JWT"} + }; + + var encodedToken = JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders); + + return encodedToken; + } + + /// + /// 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 static bool IsTokenValid(string token, string tokenId, string secret) + { + if (token == null) + return false; + + var key = ECDsa.Create(); + key?.ImportECPrivateKey(Convert.FromBase64String(secret), 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; + } + } + + private static bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken tokenToValidate, TokenValidationParameters @param) + { + if (expires != null) + { + return expires > DateTime.UtcNow; + } + return false; + } + + /// + /// 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. + /// + private string ParseKey(string key) + { + List keyLines = new List(); + keyLines.AddRange(key.Split('\n', StringSplitOptions.RemoveEmptyEntries)); + + keyLines.RemoveAt(0); + keyLines.RemoveAt(keyLines.Count - 1); + + return string.Join("", keyLines); + } + + /// + /// Generates a random hexadecimal string of the specified length. + /// + /// The number of hexadecimal digits to generate. + /// A string containing a random sequence of hexadecimal characters. + /// + /// If the specified number of digits is odd, the method will generate one extra random digit + /// to ensure the output string has the exact number of requested digits. + /// + private static string RandomHex(int digits) + { + byte[] buffer = new byte[digits / 2]; + _random.NextBytes(buffer); + string result = string.Concat(buffer.Select(x => x.ToString("X2")).ToArray()); + if (digits % 2 == 0) + return result; + return result + _random.Next(16).ToString("X"); + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// From 5c28832949f736678e63829a611cb9896f7ce7ca Mon Sep 17 00:00:00 2001 From: Romazes Date: Mon, 19 Aug 2024 17:47:04 +0300 Subject: [PATCH 03/17] test:fix: ApiTests --- .../CoinbaseApiTests.cs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs index a62a821..2d8fe60 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs @@ -41,24 +41,31 @@ public void Setup() CoinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); } - [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(System.Security.Cryptography.CryptographicException))] + public void InvalidAuthenticationCredentialsShouldThrowException(string apiKey, string apiKeySecret, Type expectedException) { - var coinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); + try + { + var coinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); - // 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,13 +168,13 @@ 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"); From 3c486c03140de935d7ebc7f49a0234be27153716 Mon Sep 17 00:00:00 2001 From: Romazes Date: Mon, 19 Aug 2024 22:10:51 +0300 Subject: [PATCH 04/17] feat: implement of JWT for WebSocket connection --- .../CoinbaseBrokerageDataQueueHandlerTests.cs | 25 +++++---- .../Api/CoinbaseApi.cs | 20 ++----- .../Api/CoinbaseApiClient.cs | 52 ++----------------- .../CoinbaseBrokerage.Messaging.cs | 6 +-- .../Models/CoinbaseSubscriptionMessage.cs | 29 +++-------- 5 files changed, 34 insertions(+), 98 deletions(-) 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/Api/CoinbaseApi.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs index 0654acd..a2289a0 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs @@ -76,26 +76,14 @@ public CoinbaseApi(ISymbolMapper symbolMapper, ISecurityProvider securityProvide } /// - /// 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.GenerateWebSocketJWTToken(); } /// diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 368c9bd..88505dc 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -99,51 +99,9 @@ public IRestResponse ExecuteRequest(IRestRequest request) return response; } - /// - /// Generates a signature for a given set of parameters using HMAC-SHA256. - /// - /// 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. - /// - /// - /// The signature is computed using the HMAC-SHA256 algorithm and is typically used for authentication and message integrity. - /// - private string GetSign(string timeStamp, string httpMethod, string urlPath, string body) - { - var preHash = timeStamp + httpMethod + urlPath + body; - - var sig = _hmacSha256.ComputeHash(Encoding.UTF8.GetBytes(preHash)); - - return Convert.ToHexString(sig).ToLowerInvariant(); - } - - public (string apiKey, string timestamp, string signature) GenerateWebSocketSignature(string channel, ICollection productIds) - { - var timestamp = GetNonce(); - - var products = string.Join(",", productIds ?? Array.Empty()); - - var signature = GetSign(timestamp, string.Empty, channel, products); - - return (_apiKey, timestamp, signature); - } - - /// - /// Generates a unique nonce based on the current UTC time in Unix timestamp format. - /// - /// - /// A string representation of the generated nonce. - /// - /// - /// The nonce is used to ensure the uniqueness of each request, typically in the context of security and authentication. - /// - private static string GetNonce() + public string GenerateWebSocketJWTToken() { - return Time.DateTimeToUnixTimeStamp(DateTime.UtcNow).ToString("F0", CultureInfo.InvariantCulture); + return GenerateWebSocketToken(_apiKey, _parsedCbPrivateKey); } private static string GenerateRestToken(string name, string secret, string uri) @@ -178,15 +136,15 @@ private static string GenerateRestToken(string name, string secret, string uri) /// Generates a JWT token with the specified name and secret using ECDsa signing. /// /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. - /// The ECDsa private key in Base64 format used to sign the token. + /// The ECDsa private key in Base64 format used to sign the token. /// A signed JWT token as a string. /// /// 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 static string GenerateWebSocketToken(string name, string secret) + private static string GenerateWebSocketToken(string name, string privateKey) { - var privateKeyBytes = Convert.FromBase64String(secret); // Assuming PEM is base64 encoded + var privateKeyBytes = Convert.FromBase64String(privateKey); // Assuming PEM is base64 encoded using var key = ECDsa.Create(); key.ImportECPrivateKey(privateKeyBytes, out _); diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs index 86ee28f..e45de30 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs @@ -347,7 +347,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 +507,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/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; } } From f4a3bd594eaf4ca7bc014ca838d63b747c6e3dcc Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 20 Aug 2024 17:59:04 +0300 Subject: [PATCH 05/17] refactor: new Auth in ApiClient --- .../Api/CoinbaseApi.cs | 2 +- .../Api/CoinbaseApiClient.cs | 129 ++++++++++++------ 2 files changed, 86 insertions(+), 45 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs index a2289a0..88f2bc5 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs @@ -83,7 +83,7 @@ public CoinbaseApi(ISymbolMapper symbolMapper, ISecurityProvider securityProvide /// public string GetWebSocketJWTToken() { - return _apiClient.GenerateWebSocketJWTToken(); + return _apiClient.GenerateWebSocketToken(); } /// diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 88505dc..6e62131 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -34,19 +34,51 @@ namespace QuantConnect.Brokerages.Coinbase.Api; /// public class CoinbaseApiClient : IDisposable { + /// + /// Provides a thread-safe random number generator instance. + /// private readonly static Random _random = new Random(); private readonly string _apiKey; + + /// + /// Stores the parsed Coinbase private key in a suitable format. + /// private readonly string _parsedCbPrivateKey; - private readonly HMACSHA256 _hmacSha256; + + /// + /// 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; + /// + /// 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 API key, + /// API key secret, REST API URL, and maximum requests per second. + /// + /// The API key required for authenticating requests. + /// The 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 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 apiKey, string apiKeySecret, string restApiUrl, int maxRequestsPerSecond) { _apiKey = apiKey; _restClient = new RestClient(restApiUrl); _parsedCbPrivateKey = ParseKey(apiKeySecret); - _hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(apiKeySecret)); _rateGate = new RateGate(maxRequestsPerSecond, Time.OneSecond); } @@ -99,70 +131,65 @@ public IRestResponse ExecuteRequest(IRestRequest request) return response; } - public string GenerateWebSocketJWTToken() + /// + /// Generates a JWT token for WebSocket connections. + /// + /// A signed JWT token as a string. + public string GenerateWebSocketToken() { - return GenerateWebSocketToken(_apiKey, _parsedCbPrivateKey); + return GenerateToken(_apiKey, _parsedCbPrivateKey); } + /// + /// Generates a JWT token for REST API requests. + /// + /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. + /// The ECDsa private key in Base64 format used to sign the token. + /// The URI to include in the token payload. + /// A signed JWT token as a string. private static string GenerateRestToken(string name, string secret, string uri) { - var privateKeyBytes = Convert.FromBase64String(secret); // Assuming PEM is base64 encoded - using var key = ECDsa.Create(); - key.ImportECPrivateKey(privateKeyBytes, out _); - - var payload = new Dictionary - { - { "sub", name }, - { "iss", "coinbase-cloud" }, - { "nbf", Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, - { "exp", Convert.ToInt64((DateTime.UtcNow.AddMinutes(1) - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, - { "uri", uri } - }; - - var extraHeaders = new Dictionary - { - { "kid", name }, - // add nonce to prevent replay attacks with a random 10 digit number - { "nonce", RandomHex(10) }, - { "typ", "JWT"} - }; - - var encodedToken = JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders); - - return encodedToken; + return GenerateToken(name, secret, uri); } /// - /// Generates a JWT token with the specified name and secret using ECDsa signing. + /// Generates a JWT token with the specified parameters using ECDsa signing. /// /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. /// The ECDsa private key in Base64 format used to sign the token. + /// The URI to include in the token payload. Pass null for WebSocket tokens. /// A signed JWT token as a string. /// - /// This method creates a JWT token with a subject, issuer, and a short expiration time, signed using the ES256 algorithm. + /// 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 static string GenerateWebSocketToken(string name, string privateKey) + private static string GenerateToken(string name, string privateKey, string uri = null) { var privateKeyBytes = Convert.FromBase64String(privateKey); // Assuming PEM is base64 encoded using var key = ECDsa.Create(); key.ImportECPrivateKey(privateKeyBytes, out _); + var utcNow = DateTime.UtcNow; var payload = new Dictionary - { - { "sub", name }, - { "iss", "coinbase-cloud" }, - { "nbf", Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, - { "exp", Convert.ToInt64((DateTime.UtcNow.AddMinutes(1) - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) }, - }; + { + { "sub", name }, + { "iss", "coinbase-cloud" }, + { "nbf", Convert.ToInt64((utcNow - EpochTime).TotalSeconds) }, + { "exp", Convert.ToInt64((utcNow.AddMinutes(1) - EpochTime).TotalSeconds) } + }; + + 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(10) }, - { "typ", "JWT"} - }; + { + { "kid", name }, + // add nonce to prevent replay attacks with a random 10 digit number + { "nonce", RandomHex(10) }, + { "typ", "JWT"} + }; var encodedToken = JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders); @@ -214,6 +241,20 @@ private static bool IsTokenValid(string token, string tokenId, string secret) } } + /// + /// 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) @@ -271,6 +312,6 @@ private static string RandomHex(int digits) /// public void Dispose() { - _hmacSha256.DisposeSafely(); + _rateGate.Dispose(); } } From c86b6d5beee858fcbc22ef9ab0d873c4102144b9 Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 20 Aug 2024 18:12:46 +0300 Subject: [PATCH 06/17] test:refactor: CoinbaseBrokerage tests --- .../CoinbaseBrokerageTests.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs index ef5da8a..b492134 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,20 +67,20 @@ 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"); @@ -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() + ); + } } } From e697578edb17dcd1534f97f6675902178e9c8aec Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 20 Aug 2024 18:26:25 +0300 Subject: [PATCH 07/17] rename: apiKey and apiSecret to name and privateKey --- .../CoinbaseApiTests.cs | 20 +++---- .../CoinbaseBrokerageAdditionalTests.cs | 6 +- .../CoinbaseBrokerageTests.cs | 8 +-- .../CoinbaseDownloader.cs | 6 +- .../CoinbaseExchangeInfoDownloader.cs | 6 +- .../Api/CoinbaseApi.cs | 4 +- .../Api/CoinbaseApiClient.cs | 56 +++++++++++-------- .../CoinbaseBrokerage.DataQueueHandler.cs | 4 +- .../CoinbaseBrokerage.cs | 28 +++++----- .../CoinbaseBrokerageFactory.cs | 6 +- 10 files changed, 78 insertions(+), 66 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs index 2d8fe60..0b2cd34 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs @@ -35,19 +35,19 @@ 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-key"); + var priavteKey = Config.Get("coinbase-api-secret"); - CoinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); + CoinbaseApi = CreateCoinbaseApi(name, priavteKey); } [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(System.Security.Cryptography.CryptographicException))] - public void InvalidAuthenticationCredentialsShouldThrowException(string apiKey, string apiKeySecret, Type expectedException) + public void InvalidAuthenticationCredentialsShouldThrowException(string name, string privateKey, Type expectedException) { try { - var coinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret); + var coinbaseApi = CreateCoinbaseApi(name, privateKey); // call random endpoint with incorrect credential Assert.Throws(expectedException, () => coinbaseApi.GetAccounts()); @@ -177,13 +177,13 @@ public void ParseWebSocketLevel2DataResponse() [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-key"); + var privateKey = Config.Get("coinbase-api-secret"); 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); @@ -206,11 +206,11 @@ public void CancelOrderWithWrongOrderId(string orderId, string errorMessage) Assert.AreEqual(errorMessage, response.FailureReason); } - private CoinbaseApi CreateCoinbaseApi(string apiKey, string apiKeySecret) + 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..ef9d836 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs @@ -71,12 +71,12 @@ 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-key"); + var privateKey = Config.Get("coinbase-api-secret"); 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, aggregator, null); } } } diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs index b492134..1ca7f30 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs @@ -82,14 +82,14 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec 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-key"); + var privateKey = Config.Get("coinbase-api-secret"); 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, new AggregationManager(), null); } /// diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs index cabbefb..345e159 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-key", ""); + var privateKey = Config.Get("coinbase-api-secret", ""); 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..0cf331f 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-key"); + var privateKey = Config.Get("coinbase-api-secret"); 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/Api/CoinbaseApi.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs index 88f2bc5..aa86f45 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs @@ -68,11 +68,11 @@ 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); } /// diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 6e62131..0e878ad 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -17,11 +17,9 @@ 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 Microsoft.IdentityModel.Tokens; @@ -38,11 +36,25 @@ public class CoinbaseApiClient : IDisposable /// Provides a thread-safe random number generator instance. /// private readonly static Random _random = new Random(); - private readonly string _apiKey; + + /// + /// 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; /// /// Stores the parsed Coinbase private key in a suitable format. /// + /// + /// This field contains the private key associated with the CDP API key, parsed from its + /// original format (typically Base64). It is used for signing requests to ensure secure + /// communication with Coinbase's API. + /// private readonly string _parsedCbPrivateKey; /// @@ -61,24 +73,24 @@ public class CoinbaseApiClient : IDisposable 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 API key, - /// API key secret, REST API URL, and maximum requests per second. + /// Initializes a new instance of the class with the specified CDP API keys, + /// REST API URL, and maximum requests per second. /// - /// The API key required for authenticating requests. - /// The API key secret used to sign requests. This will be parsed into a usable format. + /// 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. + /// 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 API key, parsing the private key, + /// 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 apiKey, string apiKeySecret, string restApiUrl, int maxRequestsPerSecond) + public CoinbaseApiClient(string name, string privateKey, string restApiUrl, int maxRequestsPerSecond) { - _apiKey = apiKey; + _name = name; _restClient = new RestClient(restApiUrl); - _parsedCbPrivateKey = ParseKey(apiKeySecret); + _parsedCbPrivateKey = ParseKey(privateKey); _rateGate = new RateGate(maxRequestsPerSecond, Time.OneSecond); } @@ -93,9 +105,9 @@ public CoinbaseApiClient(string apiKey, string apiKeySecret, string restApiUrl, private void AuthenticateRequest(IRestRequest request) { var uri = _restClient.BuildUri(request); - var generatedJWTToken = GenerateRestToken(_apiKey, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); + var generatedJWTToken = GenerateRestToken(_name, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); - if (!IsTokenValid(generatedJWTToken, _apiKey, _parsedCbPrivateKey)) + if (!IsTokenValid(generatedJWTToken, _name, _parsedCbPrivateKey)) { throw new InvalidOperationException("The generated JWT token is invalid. Authentication failed."); } @@ -137,19 +149,19 @@ public IRestResponse ExecuteRequest(IRestRequest request) /// A signed JWT token as a string. public string GenerateWebSocketToken() { - return GenerateToken(_apiKey, _parsedCbPrivateKey); + return GenerateToken(_name, _parsedCbPrivateKey); } /// /// Generates a JWT token for REST API requests. /// /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. - /// The ECDsa private key in Base64 format used to sign the token. + /// The ECDsa private key in Base64 format used to sign the token. /// The URI to include in the token payload. /// A signed JWT token as a string. - private static string GenerateRestToken(string name, string secret, string uri) + private static string GenerateRestToken(string name, string privateKey, string uri) { - return GenerateToken(name, secret, uri); + return GenerateToken(name, privateKey, uri); } /// @@ -165,7 +177,7 @@ private static string GenerateRestToken(string name, string secret, string uri) /// private static string GenerateToken(string name, string privateKey, string uri = null) { - var privateKeyBytes = Convert.FromBase64String(privateKey); // Assuming PEM is base64 encoded + var privateKeyBytes = Convert.FromBase64String(privateKey); using var key = ECDsa.Create(); key.ImportECPrivateKey(privateKeyBytes, out _); var utcNow = DateTime.UtcNow; @@ -201,7 +213,7 @@ private static string GenerateToken(string name, string privateKey, string uri = /// /// 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. + /// The ECDsa private key in Base64 format used to validate the token's signature. /// /// true if the token is successfully validated; otherwise, false. /// @@ -209,13 +221,13 @@ private static string GenerateToken(string name, string privateKey, string uri = /// 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 static bool IsTokenValid(string token, string tokenId, string secret) + private static bool IsTokenValid(string token, string tokenId, string parsedPrivateKey) { if (token == null) return false; var key = ECDsa.Create(); - key?.ImportECPrivateKey(Convert.FromBase64String(secret), out _); + key?.ImportECPrivateKey(Convert.FromBase64String(parsedPrivateKey), out _); var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId }; diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs index 6877a44..aeec9cb 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs @@ -76,8 +76,8 @@ public void SetJob(LiveNodePacket job) Initialize( webSocketUrl: job.BrokerageData["coinbase-url"], - apiKey: job.BrokerageData["coinbase-api-key"], - apiSecret: job.BrokerageData["coinbase-api-secret"], + name: job.BrokerageData["coinbase-api-key"], + privateKey: job.BrokerageData["coinbase-api-secret"], restApiUrl: job.BrokerageData["coinbase-rest-api"], algorithm: null, orderProvider: null, diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs index b1e035c..e2be0f6 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs @@ -89,15 +89,15 @@ 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, + public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, string restApiUrl, IAlgorithm algorithm, IDataAggregator aggregator, LiveNodePacket job) - : this(webSocketUrl, apiKey, apiSecret, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, aggregator, job) + : this(webSocketUrl, name, privateKey, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, aggregator, job) { } @@ -106,21 +106,21 @@ 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, + public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, string restApiUrl, IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) : base(MarketName) { Initialize( webSocketUrl: webSocketUrl, - apiKey: apiKey, - apiSecret: apiSecret, + name: name, + privateKey: privateKey, restApiUrl: restApiUrl, algorithm: algorithm, orderProvider: orderProvider, @@ -133,13 +133,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, + protected void Initialize(string webSocketUrl, string name, string privateKey, string restApiUrl, IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) { if (IsInitialized) @@ -149,13 +149,13 @@ 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; _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..c370e6f 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs @@ -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-key", errors); + var privateKey = Read(job.BrokerageData, "coinbase-api-secret", errors); var apiUrl = Read(job.BrokerageData, "coinbase-rest-api", errors); var wsUrl = Read(job.BrokerageData, "coinbase-url", errors); @@ -84,7 +84,7 @@ public override IBrokerage CreateBrokerage(Packets.LiveNodePacket job, IAlgorith 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, aggregator, job); // Add the brokerage to the composer to ensure its accessible to the live data feed. Composer.Instance.AddPart(brokerage); From 93b187ae8d5552c62be799ca31987a00e57acfdd Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 20 Aug 2024 19:02:21 +0300 Subject: [PATCH 08/17] rename: config name of keys --- .github/workflows/gh-actions.yml | 4 ++-- QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs | 8 ++++---- .../CoinbaseBrokerageAdditionalTests.cs | 4 ++-- .../CoinbaseBrokerageHistoryProviderTests.cs | 4 ++-- .../CoinbaseBrokerageTests.cs | 4 ++-- QuantConnect.CoinbaseBrokerage.Tests/config.json | 4 ++-- .../CoinbaseDownloader.cs | 4 ++-- .../CoinbaseExchangeInfoDownloader.cs | 4 ++-- QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs | 2 +- .../CoinbaseBrokerage.DataQueueHandler.cs | 4 ++-- .../CoinbaseBrokerageFactory.cs | 8 ++++---- 11 files changed, 25 insertions(+), 25 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 0b2cd34..f1e3ffc 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs @@ -35,8 +35,8 @@ public class CoinbaseApiTests [SetUp] public void Setup() { - var name = Config.Get("coinbase-api-key"); - var priavteKey = Config.Get("coinbase-api-secret"); + var name = Config.Get("coinbase-api-name"); + var priavteKey = Config.Get("coinbase-api-private-key"); CoinbaseApi = CreateCoinbaseApi(name, priavteKey); } @@ -177,8 +177,8 @@ public void ParseWebSocketLevel2DataResponse() [TestCase("/api/v3/brokerage/orders", "[]", "Bad Request")] public void ValidateCoinbaseRestRequestWithWrongBodyParameter(string uriPath, object bodyData, string message) { - var name = Config.Get("coinbase-api-key"); - var privateKey = 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); diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs index ef9d836..0b744b0 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs @@ -71,8 +71,8 @@ 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 name = Config.Get("coinbase-api-key"); - var privateKey = 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(); diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs index 3844154..4bf640d 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs @@ -36,8 +36,8 @@ 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(), diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs index 1ca7f30..c28168c 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs @@ -82,8 +82,8 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securityManager, transactions, algorithmSettings)); algorithm.Setup(a => a.Securities).Returns(securityManager); - var name = Config.Get("coinbase-api-key"); - var privateKey = 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"); 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 345e159..dad8b45 100644 --- a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs @@ -92,8 +92,8 @@ public IEnumerable Get(DataDownloaderGetParameters dataDownloaderGetPa /// private Brokerage CreateBrokerage() { - var name = Config.Get("coinbase-api-key", ""); - var privateKey = 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, name, privateKey, restApiUrl, null, null, null); } diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs index 0cf331f..e797e4d 100644 --- a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs @@ -66,8 +66,8 @@ public IEnumerable Get() /// private CoinbaseApi CreateCoinbaseApi() { - var name = Config.Get("coinbase-api-key"); - var privateKey = 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, 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/CoinbaseBrokerage.DataQueueHandler.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs index aeec9cb..ce08036 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs @@ -76,8 +76,8 @@ public void SetJob(LiveNodePacket job) Initialize( webSocketUrl: job.BrokerageData["coinbase-url"], - name: job.BrokerageData["coinbase-api-key"], - privateKey: 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, diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs index c370e6f..40b1b16 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 name = Read(job.BrokerageData, "coinbase-api-key", errors); - var privateKey = 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); From b1211ca548cbd3ca26a8744fb15d7060b7efb590 Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 20 Aug 2024 22:58:03 +0300 Subject: [PATCH 09/17] remove: extra nuget to validate JWT token test:feat: validate JWT token --- .../CoinbaseApiTests.cs | 87 +++++++++++++++++++ ...uantConnect.CoinbaseBrokerage.Tests.csproj | 2 + .../Api/CoinbaseApiClient.cs | 81 +---------------- .../QuantConnect.CoinbaseBrokerage.csproj | 7 +- 4 files changed, 94 insertions(+), 83 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs index f1e3ffc..ac4e09a 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; @@ -206,6 +209,90 @@ public void CancelOrderWithWrongOrderId(string orderId, string errorMessage) Assert.AreEqual(errorMessage, response.FailureReason); } + [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"); 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/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 0e878ad..e0e9f2e 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -22,8 +22,9 @@ using System.Diagnostics; using System.Collections.Generic; using System.Security.Cryptography; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("QuantConnect.Brokerages.Coinbase.Tests")] namespace QuantConnect.Brokerages.Coinbase.Api; @@ -106,12 +107,6 @@ private void AuthenticateRequest(IRestRequest request) { var uri = _restClient.BuildUri(request); var generatedJWTToken = GenerateRestToken(_name, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); - - if (!IsTokenValid(generatedJWTToken, _name, _parsedCbPrivateKey)) - { - throw new InvalidOperationException("The generated JWT token is invalid. Authentication failed."); - } - request.AddOrUpdateHeader("Authorization", "Bearer " + generatedJWTToken); } @@ -208,74 +203,6 @@ private static string GenerateToken(string name, string privateKey, string uri = return encodedToken; } - /// - /// 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 static 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; - } - /// /// Parses a key string by removing the first and last lines and returning the remaining content as a single string. /// @@ -286,7 +213,7 @@ private static bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expir /// 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. /// - private string ParseKey(string key) + internal string ParseKey(string key) { List keyLines = new List(); keyLines.AddRange(key.Split('\n', StringSplitOptions.RemoveEmptyEntries)); diff --git a/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj index 076ac1d..0e5275f 100644 --- a/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj +++ b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj @@ -23,16 +23,11 @@ bin\Release\ - + - - \ No newline at end of file From 6077f13263d39d7e643a01301b9e435e692d887a Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 20 Aug 2024 23:04:25 +0300 Subject: [PATCH 10/17] fix: missed OrderStatus in Order WS response --- QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs | 7 +++++++ 1 file changed, 7 insertions(+) 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, } From 874a796e0feef3fb8b691640060f715999dad914 Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 00:36:47 +0300 Subject: [PATCH 11/17] refactor: use aggregator instance like part of Instance --- .../CoinbaseBrokerageAdditionalTests.cs | 3 +-- .../CoinbaseBrokerageHistoryProviderTests.cs | 2 -- .../CoinbaseBrokerageTests.cs | 2 +- .../CoinbaseBrokerage.DataQueueHandler.cs | 4 ---- .../CoinbaseBrokerage.cs | 23 +++++++++++-------- .../CoinbaseBrokerageFactory.cs | 6 +---- 6 files changed, 17 insertions(+), 23 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs index 0b744b0..cf10e32 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs @@ -74,9 +74,8 @@ private static CoinbaseBrokerage GetBrokerage() 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, name, privateKey, restApiUrl, algorithm, aggregator, null); + return new CoinbaseBrokerage(wssUrl, name, privateKey, restApiUrl, algorithm, null); } } } diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs index 4bf640d..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 { @@ -40,7 +39,6 @@ public void GetsHistory(Symbol symbol, Resolution resolution, TickType tickType, 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 c28168c..295a7d5 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs @@ -89,7 +89,7 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec _api = new CoinbaseApi(SymbolMapper, null, name, privateKey, restApiUrl); - return new CoinbaseBrokerage(webSocketUrl, name, privateKey, restApiUrl, algorithm.Object, orderProvider, new AggregationManager(), null); + return new CoinbaseBrokerage(webSocketUrl, name, privateKey, restApiUrl, algorithm.Object, orderProvider, null); } /// diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs index ce08036..d2432fa 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs @@ -71,9 +71,6 @@ 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"], name: job.BrokerageData["coinbase-api-name"], @@ -81,7 +78,6 @@ public void SetJob(LiveNodePacket job) restApiUrl: job.BrokerageData["coinbase-rest-api"], algorithm: null, orderProvider: null, - aggregator: aggregator, job: job ); diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs index e2be0f6..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; @@ -93,11 +94,10 @@ public CoinbaseBrokerage() : base(MarketName) /// 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 name, string privateKey, string restApiUrl, - IAlgorithm algorithm, IDataAggregator aggregator, LiveNodePacket job) - : this(webSocketUrl, name, privateKey, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, aggregator, job) + IAlgorithm algorithm, LiveNodePacket job) + : this(webSocketUrl, name, privateKey, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, job) { } @@ -111,10 +111,9 @@ public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, st /// 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 name, string privateKey, string restApiUrl, - IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) + IAlgorithm algorithm, IOrderProvider orderProvider, LiveNodePacket job) : base(MarketName) { Initialize( @@ -124,7 +123,6 @@ public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, st restApiUrl: restApiUrl, algorithm: algorithm, orderProvider: orderProvider, - aggregator: aggregator, job: job ); } @@ -137,10 +135,9 @@ public CoinbaseBrokerage(string webSocketUrl, string name, string privateKey, st /// 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 name, string privateKey, string restApiUrl, - IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) + IAlgorithm algorithm, IOrderProvider orderProvider, LiveNodePacket job) { if (IsInitialized) { @@ -153,7 +150,15 @@ protected void Initialize(string webSocketUrl, string name, string privateKey, s _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, name, privateKey, restApiUrl); OrderProvider = orderProvider; diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs index 40b1b16..6ffd751 100644 --- a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs @@ -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, name, privateKey, 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); From e9aada38066ffc23a97cb86eb0ee44fff6e147fc Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 22:23:38 +0300 Subject: [PATCH 12/17] remove: extra GenerateRestToken in ApiClient --- .../Api/CoinbaseApiClient.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index e0e9f2e..a4e9f0d 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -106,7 +106,7 @@ public CoinbaseApiClient(string name, string privateKey, string restApiUrl, int private void AuthenticateRequest(IRestRequest request) { var uri = _restClient.BuildUri(request); - var generatedJWTToken = GenerateRestToken(_name, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); + var generatedJWTToken = GenerateToken(_name, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); request.AddOrUpdateHeader("Authorization", "Bearer " + generatedJWTToken); } @@ -147,18 +147,6 @@ public string GenerateWebSocketToken() return GenerateToken(_name, _parsedCbPrivateKey); } - /// - /// Generates a JWT token for REST API requests. - /// - /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. - /// The ECDsa private key in Base64 format used to sign the token. - /// The URI to include in the token payload. - /// A signed JWT token as a string. - private static string GenerateRestToken(string name, string privateKey, string uri) - { - return GenerateToken(name, privateKey, uri); - } - /// /// Generates a JWT token with the specified parameters using ECDsa signing. /// From 3854636dc2da77c2db754357c2a899e4c7b26d0c Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 23:00:16 +0300 Subject: [PATCH 13/17] refactor: reuse constant variable to save performance --- .../Api/CoinbaseApiClient.cs | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index a4e9f0d..3cd1738 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -49,14 +49,10 @@ public class CoinbaseApiClient : IDisposable private readonly string _name; /// - /// Stores the parsed Coinbase private key in a suitable format. + /// 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. /// - /// - /// This field contains the private key associated with the CDP API key, parsed from its - /// original format (typically Base64). It is used for signing requests to ensure secure - /// communication with Coinbase's API. - /// - private readonly string _parsedCbPrivateKey; + private readonly ECDsa _privateKey; /// /// Represents the REST client used to send HTTP requests to the Coinbase API. @@ -91,8 +87,10 @@ public CoinbaseApiClient(string name, string privateKey, string restApiUrl, int { _name = name; _restClient = new RestClient(restApiUrl); - _parsedCbPrivateKey = ParseKey(privateKey); _rateGate = new RateGate(maxRequestsPerSecond, Time.OneSecond); + + _privateKey = ECDsa.Create(); + _privateKey.ImportECPrivateKey(Convert.FromBase64String(ParseKey(privateKey)), out _); } /// @@ -106,7 +104,7 @@ public CoinbaseApiClient(string name, string privateKey, string restApiUrl, int private void AuthenticateRequest(IRestRequest request) { var uri = _restClient.BuildUri(request); - var generatedJWTToken = GenerateToken(_name, _parsedCbPrivateKey, $"{request.Method} {uri.Host + uri.AbsolutePath}"); + var generatedJWTToken = GenerateToken($"{request.Method} {uri.Host + uri.AbsolutePath}"); request.AddOrUpdateHeader("Authorization", "Bearer " + generatedJWTToken); } @@ -144,30 +142,25 @@ public IRestResponse ExecuteRequest(IRestRequest request) /// A signed JWT token as a string. public string GenerateWebSocketToken() { - return GenerateToken(_name, _parsedCbPrivateKey); + return GenerateToken(); } /// /// Generates a JWT token with the specified parameters using ECDsa signing. /// - /// The name to be used as the subject ("sub") and key identifier ("kid") in the token payload and headers. - /// The ECDsa private key in Base64 format used to sign the token. /// The URI to include in the token payload. Pass null for WebSocket tokens. /// A signed JWT token as a string. /// /// 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 static string GenerateToken(string name, string privateKey, string uri = null) + private string GenerateToken(string uri = null) { - var privateKeyBytes = Convert.FromBase64String(privateKey); - using var key = ECDsa.Create(); - key.ImportECPrivateKey(privateKeyBytes, out _); var utcNow = DateTime.UtcNow; var payload = new Dictionary { - { "sub", name }, + { "sub", _name }, { "iss", "coinbase-cloud" }, { "nbf", Convert.ToInt64((utcNow - EpochTime).TotalSeconds) }, { "exp", Convert.ToInt64((utcNow.AddMinutes(1) - EpochTime).TotalSeconds) } @@ -180,13 +173,13 @@ private static string GenerateToken(string name, string privateKey, string uri = var extraHeaders = new Dictionary { - { "kid", name }, + { "kid", _name }, // add nonce to prevent replay attacks with a random 10 digit number { "nonce", RandomHex(10) }, { "typ", "JWT"} }; - var encodedToken = JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders); + var encodedToken = JWT.Encode(payload, _privateKey, JwsAlgorithm.ES256, extraHeaders); return encodedToken; } @@ -240,5 +233,6 @@ private static string RandomHex(int digits) public void Dispose() { _rateGate.Dispose(); + _privateKey.Dispose(); } } From e448f682ad01b8de9afac31ee918bbef374adffe Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 23:14:18 +0300 Subject: [PATCH 14/17] refactor: RandomHex() --- .../Api/CoinbaseApiClient.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 3cd1738..ddf0a8e 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -206,22 +206,16 @@ internal string ParseKey(string key) } /// - /// Generates a random hexadecimal string of the specified length. + /// Generates a random hexadecimal string of a fixed length. /// - /// The number of hexadecimal digits to generate. - /// A string containing a random sequence of hexadecimal characters. - /// - /// If the specified number of digits is odd, the method will generate one extra random digit - /// to ensure the output string has the exact number of requested digits. - /// - private static string RandomHex(int digits) + /// + /// A representing a random hexadecimal value of 10 characters. + /// + private static string RandomHex() { - byte[] buffer = new byte[digits / 2]; + byte[] buffer = new byte[10 / 2]; _random.NextBytes(buffer); - string result = string.Concat(buffer.Select(x => x.ToString("X2")).ToArray()); - if (digits % 2 == 0) - return result; - return result + _random.Next(16).ToString("X"); + return string.Concat(buffer.Select(x => x.ToString("X2")).ToArray()); } /// From a91b2c25fd28be98b0aae5bcb8e333a8410f0f39 Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 23:50:02 +0300 Subject: [PATCH 15/17] remove: parameter in RandomHex --- QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index ddf0a8e..0c8df74 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -1,4 +1,4 @@ -/* +/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2023 QuantConnect Corporation. * @@ -175,7 +175,7 @@ private string GenerateToken(string uri = null) { { "kid", _name }, // add nonce to prevent replay attacks with a random 10 digit number - { "nonce", RandomHex(10) }, + { "nonce", RandomHex() }, { "typ", "JWT"} }; From bf4bc817dad3f9a5b07136a27015d5766d1a287a Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 23:51:22 +0300 Subject: [PATCH 16/17] refactor: ParseKey() in ApiClient --- .../CoinbaseApiTests.cs | 3 ++- .../Api/CoinbaseApiClient.cs | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs index ac4e09a..7814947 100644 --- a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs @@ -45,7 +45,8 @@ public void Setup() } [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(System.Security.Cryptography.CryptographicException))] + [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) { try diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs index 0c8df74..19f89d5 100644 --- a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -1,4 +1,4 @@ -/* +/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2023 QuantConnect Corporation. * @@ -196,11 +196,14 @@ private string GenerateToken(string uri = null) /// internal string ParseKey(string key) { - List keyLines = new List(); - keyLines.AddRange(key.Split('\n', StringSplitOptions.RemoveEmptyEntries)); + var keyLines = key.Split('\n', StringSplitOptions.RemoveEmptyEntries).ToList(); - keyLines.RemoveAt(0); - keyLines.RemoveAt(keyLines.Count - 1); + // 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 string.Join("", keyLines); } From 9ebde0148dce73df5f3002edbd073444b0abbdde Mon Sep 17 00:00:00 2001 From: Romazes Date: Wed, 21 Aug 2024 23:57:41 +0300 Subject: [PATCH 17/17] feat: condition to prevent creating string WS response --- .../CoinbaseBrokerage.Messaging.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs index e45de30..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 {