diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index f18757be..2ba6a70e 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -16,7 +16,9 @@ The above copyright notice and this permission notice shall be included in all c using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Net; using System.Threading.Tasks; +using System.Xml.Linq; namespace ExchangeSharp { @@ -25,10 +27,10 @@ namespace ExchangeSharp /// If you are using legacy API keys from previous Coinbase versions they must be upgraded to Advanced Trade on the Coinbase site. /// These keys must be set before using the Coinbase API (sorry). /// - public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI + public partial class ExchangeCoinbaseAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.coinbase.com/api/v3/brokerage"; - private readonly string BaseUrlV2 = "https://api.coinbase.com/v2"; // For Wallet Support + protected readonly string BaseUrlV2 = "https://api.coinbase.com/v2"; // For Wallet Support public override string BaseUrlWebSocket { get; set; } = "wss://advanced-trade-ws.coinbase.com"; private enum PaginationType { None, V2, V3} @@ -37,7 +39,7 @@ private enum PaginationType { None, V2, V3} private Dictionary Accounts = null; // Cached Account IDs - private ExchangeCoinbaseAPI() + protected ExchangeCoinbaseAPI() { MarketSymbolIsUppercase = true; MarketSymbolIsReversed = false; @@ -85,19 +87,11 @@ protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary payload) { if (CanMakeAuthenticatedRequest(payload)) - { - string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site - string body = CryptoUtility.GetJsonForPayload(payload); - - // V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly) - string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath; - string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString()); - - request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString()); - request.AddHeader("CB-ACCESS-SIGN", signature); - request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp); - if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body); - } + { + string endpoint = $"{request.RequestUri.Host}{request.RequestUri.AbsolutePath}"; + string token = GenerateToken(PublicApiKey.ToUnsecureString(), PrivateApiKey.ToUnsecureString(), $"{request.Method} {endpoint}"); + request.AddHeader("Authorization", $"Bearer {token}"); + } } /// diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_Const.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_Const.cs index fecffefd..52c20494 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_Const.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_Const.cs @@ -1,6 +1,6 @@ namespace ExchangeSharp { - public sealed partial class ExchangeCoinbaseAPI + public partial class ExchangeCoinbaseAPI { private const string ADVFILL = "advanced_trade_fill"; private const string AMOUNT = "amount"; diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_JWT.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_JWT.cs new file mode 100644 index 00000000..e790f3e3 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_JWT.cs @@ -0,0 +1,97 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; +using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using System.IO; + +namespace ExchangeSharp +{ + public partial class ExchangeCoinbaseAPI + { // Currently using .NET 4.7.2 version of code from https://docs.cdp.coinbase.com/advanced-trade/docs/rest-api-auth + // since we currently target netstandard2.0. If we upgrade in the future, we can change to the simpler .NET core code + static string GenerateToken(string name, string privateKeyPem, string uri) + { + // Load EC private key using BouncyCastle + var ecPrivateKey = LoadEcPrivateKeyFromPem(privateKeyPem); + + // Create security key from the manually created ECDsa + var ecdsa = GetECDsaFromPrivateKey(ecPrivateKey); + var securityKey = new ECDsaSecurityKey(ecdsa); + + // Signing credentials + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256); + + var now = DateTimeOffset.UtcNow; + + // Header and payload + var header = new JwtHeader(credentials); + header["kid"] = name; + header["nonce"] = GenerateNonce(); // Generate dynamic nonce + + var payload = new JwtPayload + { + { "iss", "coinbase-cloud" }, + { "sub", name }, + { "nbf", now.ToUnixTimeSeconds() }, + { "exp", now.AddMinutes(2).ToUnixTimeSeconds() }, + { "uri", uri } + }; + + var token = new JwtSecurityToken(header, payload); + + var tokenHandler = new JwtSecurityTokenHandler(); + return tokenHandler.WriteToken(token); + } + + // Method to generate a dynamic nonce + static string GenerateNonce(int length = 64) + { + byte[] nonceBytes = new byte[length / 2]; // Allocate enough space for the desired length (in hex characters) + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(nonceBytes); + } + return BitConverter.ToString(nonceBytes).Replace("-", "").ToLower(); // Convert byte array to hex string + } + + // Method to load EC private key from PEM using BouncyCastle + static ECPrivateKeyParameters LoadEcPrivateKeyFromPem(string privateKeyPem) + { + using (var stringReader = new StringReader(privateKeyPem)) + { + var pemReader = new PemReader(stringReader); + var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair; + if (keyPair == null) + throw new InvalidOperationException("Failed to load EC private key from PEM"); + + return (ECPrivateKeyParameters)keyPair.Private; + } + } + + // Method to convert ECPrivateKeyParameters to ECDsa + static ECDsa GetECDsaFromPrivateKey(ECPrivateKeyParameters privateKey) + { + var q = privateKey.Parameters.G.Multiply(privateKey.D).Normalize(); + var qx = q.AffineXCoord.GetEncoded(); + var qy = q.AffineYCoord.GetEncoded(); + + var ecdsaParams = new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, // Adjust if you're using a different curve + Q = + { + X = qx, + Y = qy + }, + D = privateKey.D.ToByteArrayUnsigned() + }; + + return ECDsa.Create(ecdsaParams); + } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseExchangeAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseExchangeAPI.cs new file mode 100644 index 00000000..b34016ef --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseExchangeAPI.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeSharp.Coinbase +{ + /// + /// partial implementation for Coinbase Exchange, which is for businesses (rather than Advanced which is for individuals). Since there may not be many users of Coinbase Exchange, will not expose this for now to avoid confusion + /// + public sealed partial class ExchangeCoinbaseExchangeAPI : ExchangeCoinbaseAPI + { + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { // Coinbase Exchange uses the old signing method rather than JWT + if (CanMakeAuthenticatedRequest(payload)) + { + string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site + string body = CryptoUtility.GetJsonForPayload(payload); + + // V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly) + string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath; + string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString()); + + request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString()); + request.AddHeader("CB-ACCESS-SIGN", signature); + request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp); + if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body); + } + } + } +} diff --git a/src/ExchangeSharp/ExchangeSharp.csproj b/src/ExchangeSharp/ExchangeSharp.csproj index 3fd47718..f3caff60 100644 --- a/src/ExchangeSharp/ExchangeSharp.csproj +++ b/src/ExchangeSharp/ExchangeSharp.csproj @@ -33,6 +33,7 @@ + all @@ -42,6 +43,7 @@ +