From c0af503169f195a1a79039cd7b621a118bb2a7e6 Mon Sep 17 00:00:00 2001 From: David Pang Date: Mon, 28 Apr 2025 17:46:27 -0400 Subject: [PATCH 01/10] WIP generate JWT for authentication --- xchange-coinbase/pom.xml | 70 +++++++----- .../xchange/coinbase/v2/CoinbaseV2Digest.java | 101 +++++++++++++++++- 2 files changed, 140 insertions(+), 31 deletions(-) diff --git a/xchange-coinbase/pom.xml b/xchange-coinbase/pom.xml index 49e03a95204..e66c6dceecd 100644 --- a/xchange-coinbase/pom.xml +++ b/xchange-coinbase/pom.xml @@ -1,36 +1,52 @@ - + - 4.0.0 + 4.0.0 - - org.knowm.xchange - xchange-parent - 5.2.2-SNAPSHOT - + + org.knowm.xchange + xchange-parent + 5.2.2-SNAPSHOT + - xchange-coinbase + xchange-coinbase - XChange Coinbase - XChange implementation for Coinbase + XChange Coinbase + XChange implementation for Coinbase - http://knowm.org/open-source/xchange/ - 2012 - - - Knowm Inc. - http://knowm.org/open-source/xchange/ - - - - + http://knowm.org/open-source/xchange/ + 2012 - - org.knowm.xchange - xchange-core - ${project.version} - - - + + Knowm Inc. + http://knowm.org/open-source/xchange/ + + + + + + + org.knowm.xchange + xchange-core + ${project.version} + + + com.auth0 + java-jwt + + + org.bouncycastle + bcprov-jdk15on + 1.70 + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + + + diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java index fe7cbede7fb..85d2307cb80 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java @@ -2,13 +2,42 @@ import static org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated.CB_ACCESS_TIMESTAMP; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; import jakarta.ws.rs.HeaderParam; +import java.io.StringReader; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.Security; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.knowm.xchange.service.BaseParamsDigest; import org.knowm.xchange.utils.DigestUtils; import si.mazi.rescu.RestInvocation; public class CoinbaseV2Digest extends BaseParamsDigest { + static { + // register BC once for PEM parsing + EC math + Security.addProvider(new BouncyCastleProvider()); + } + private CoinbaseV2Digest(String secretKey) { super(secretKey, HMAC_SHA_256); } @@ -19,12 +48,76 @@ public static CoinbaseV2Digest createInstance(String secretKey) { @Override public String digestParams(RestInvocation restInvocation) { - final String pathWithQueryString = - restInvocation.getInvocationUrl().replace(restInvocation.getBaseUrl(), ""); - final String timestamp = - restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP).toString(); + final String pathWithQueryString = restInvocation.getInvocationUrl() + .replace(restInvocation.getBaseUrl(), ""); + final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP) + .toString(); final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; + String requestMethod = restInvocation.getHttpMethod(); + String url = restInvocation.getInvocationUrl(); + return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); } + + /** + * @param requestMethod e.g. "GET" + * @param url e.g. "api.coinbase.com/api/v3/brokerage/accounts" + * @param privateKeyPEM your PEM string, either "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" + * @param name your key‐ID / kid & sub claim + */ + public String generateJWT(String requestMethod, String url, String privateKeyPEM, String name) + throws Exception { + // 1. Parse PEM and extract EC keypair (or derive public if only private) + PEMParser pemParser = new PEMParser(new StringReader(privateKeyPEM)); + Object parsed = pemParser.readObject(); + pemParser.close(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + + ECPrivateKey privateKey; + ECPublicKey publicKey; + + if (parsed instanceof PEMKeyPair) { + KeyPair kp = converter.getKeyPair((PEMKeyPair) parsed); + privateKey = (ECPrivateKey) kp.getPrivate(); + publicKey = (ECPublicKey) kp.getPublic(); + } else if (parsed instanceof PrivateKeyInfo) { + // only private info → convert + derive public + privateKey = (ECPrivateKey) converter.getPrivateKey((PrivateKeyInfo) parsed); + + // curve parameters for P-256 / secp256r1 + ECNamedCurveParameterSpec ecP = ECNamedCurveTable.getParameterSpec("P-256"); + ECParameterSpec bcSpec = new ECNamedCurveParameterSpec("P-256", ecP.getCurve(), ecP.getG(), ecP.getN(), ecP.getH(), ecP.getSeed()); + + // do Q = d * G + BigInteger d = privateKey.getS(); + org.bouncycastle.math.ec.ECPoint Q = ecP.getG().multiply(d).normalize(); + java.security.spec.ECPoint w = new java.security.spec.ECPoint( + Q.getAffineXCoord().toBigInteger(), Q.getAffineYCoord().toBigInteger()); + + KeyFactory kf = KeyFactory.getInstance("EC", "BC"); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, bcSpec); + publicKey = (ECPublicKey) kf.generatePublic(pubSpec); + } else { + throw new IllegalArgumentException("Unrecognized PEM object: " + parsed.getClass()); + } + + // 2. Build header & claims + long nowEpoch = Instant.now().getEpochSecond(); + Map header = new HashMap<>(); + header.put("alg", "ES256"); + header.put("typ", "JWT"); + header.put("kid", name); + header.put("nonce", String.valueOf(nowEpoch)); + + Instant notBefore = Instant.ofEpochSecond(nowEpoch); + Instant expiresAt = notBefore.plusSeconds(120); + String uri = requestMethod + " " + url; + + // 3. Sign with java-jwt + Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); + return JWT.create().withHeader(header).withIssuer("cdp").withSubject(name) + .withNotBefore(Date.from(notBefore)).withExpiresAt(Date.from(expiresAt)) + .withClaim("uri", uri).sign(alg); + } } From 929d1ae5e5477e6bd3ac3a1619d114b1eea77374 Mon Sep 17 00:00:00 2001 From: David Pang Date: Tue, 29 Apr 2025 11:59:10 -0400 Subject: [PATCH 02/10] create exchange with api key and api secret present --- .../coinbase/v2/services/AccountServiceIntegration.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java index 852e4f5965c..13b5efafc61 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java @@ -8,6 +8,7 @@ import org.junit.Test; import org.knowm.xchange.Exchange; import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.ExchangeSpecification; import org.knowm.xchange.coinbase.v2.CoinbaseExchange; import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; @@ -24,8 +25,9 @@ public class AccountServiceIntegration { @BeforeClass public static void beforeClass() { - exchange = ExchangeFactory.INSTANCE.createExchange(CoinbaseExchange.class); - AuthUtils.setApiAndSecretKey(exchange.getExchangeSpecification()); + ExchangeSpecification exchangeSpecification = ExchangeFactory.INSTANCE.createExchange(CoinbaseExchange.class).getDefaultExchangeSpecification(); + AuthUtils.setApiAndSecretKey(exchangeSpecification); + exchange = ExchangeFactory.INSTANCE.createExchange(exchangeSpecification); accountService = exchange.getAccountService(); } From 7c3139713445b57a5af64a78be29e1b5dc906ca9 Mon Sep 17 00:00:00 2001 From: David Pang Date: Tue, 29 Apr 2025 11:59:51 -0400 Subject: [PATCH 03/10] wrapped up code to generate authentication jwt --- .../xchange/coinbase/v2/CoinbaseV2Digest.java | 160 ++++++++++-------- 1 file changed, 94 insertions(+), 66 deletions(-) diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java index 85d2307cb80..925cea4132b 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java @@ -5,25 +5,23 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import jakarta.ws.rs.HeaderParam; +import java.io.IOException; import java.io.StringReader; -import java.math.BigInteger; +import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.Security; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.jce.ECNamedCurveTable; -import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; -import org.bouncycastle.jce.spec.ECNamedCurveSpec; -import org.bouncycastle.jce.spec.ECParameterSpec; -import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; @@ -46,78 +44,108 @@ public static CoinbaseV2Digest createInstance(String secretKey) { return secretKey == null ? null : new CoinbaseV2Digest(secretKey); } - @Override - public String digestParams(RestInvocation restInvocation) { - final String pathWithQueryString = restInvocation.getInvocationUrl() - .replace(restInvocation.getBaseUrl(), ""); - final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP) - .toString(); - final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; - - String requestMethod = restInvocation.getHttpMethod(); - String url = restInvocation.getInvocationUrl(); - - return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); + /** + * Load an EC keypair from either a PEMKeyPair or a raw PrivateKeyInfo, deriving the public key on + * the P-256 curve if necessary. + */ + private static KeyPair loadECKeyPair(String privateKeyPEM) + throws IOException, GeneralSecurityException { + try (PEMParser parser = new PEMParser(new StringReader(privateKeyPEM))) { + Object obj = parser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + + ECPrivateKey priv; + ECPublicKey pub; + + if (obj instanceof PEMKeyPair) { + KeyPair kp = converter.getKeyPair((PEMKeyPair) obj); + return kp; + } + + if (obj instanceof PrivateKeyInfo) { + // 1) get the private key + priv = (ECPrivateKey) converter.getPrivateKey((PrivateKeyInfo) obj); + + // 2) derive BC curve + base point + ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec("P-256"); + org.bouncycastle.math.ec.ECPoint Q = bcSpec.getG().multiply(priv.getS()).normalize(); + + // 3) convert BC point → JCA point + java.security.spec.ECPoint w = new java.security.spec.ECPoint( + Q.getAffineXCoord().toBigInteger(), Q.getAffineYCoord().toBigInteger()); + + // 4) build JCA curve spec from BC parameters + java.security.spec.EllipticCurve curve = new java.security.spec.EllipticCurve( + new ECFieldFp(bcSpec.getCurve().getField().getCharacteristic()), + bcSpec.getCurve().getA().toBigInteger(), bcSpec.getCurve().getB().toBigInteger(), + bcSpec.getSeed()); + java.security.spec.ECParameterSpec jcaSpec = new java.security.spec.ECParameterSpec(curve, + new java.security.spec.ECPoint(bcSpec.getG().getAffineXCoord().toBigInteger(), + bcSpec.getG().getAffineYCoord().toBigInteger()), bcSpec.getN(), + bcSpec.getH().intValue()); + + // 5) build the JCA public‐key spec and generate it + java.security.spec.ECPublicKeySpec pubSpec = new java.security.spec.ECPublicKeySpec(w, + jcaSpec); + KeyFactory kf = KeyFactory.getInstance("EC", "BC"); + pub = (ECPublicKey) kf.generatePublic(pubSpec); + + return new KeyPair(pub, priv); + } + + throw new IllegalArgumentException("Unknown PEM object: " + obj.getClass()); + } } /** - * @param requestMethod e.g. "GET" - * @param url e.g. "api.coinbase.com/api/v3/brokerage/accounts" - * @param privateKeyPEM your PEM string, either "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" - * @param name your key‐ID / kid & sub claim + * Generate an ES256 JWT for Coinbase Advanced Trading authentication. + * + * @param method HTTP method (e.g. "GET") + * @param url Full URL being called + * @param privateKeyPEM The PEM‐encoded EC private key (PKCS#8 or EC PRIVATE KEY) + * @param keyId Your API key ID (goes into the kid header, sub claim) */ - public String generateJWT(String requestMethod, String url, String privateKeyPEM, String name) + public static String generateJWT(String method, String url, String privateKeyPEM, String keyId) throws Exception { - // 1. Parse PEM and extract EC keypair (or derive public if only private) - PEMParser pemParser = new PEMParser(new StringReader(privateKeyPEM)); - Object parsed = pemParser.readObject(); - pemParser.close(); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); - - ECPrivateKey privateKey; - ECPublicKey publicKey; - - if (parsed instanceof PEMKeyPair) { - KeyPair kp = converter.getKeyPair((PEMKeyPair) parsed); - privateKey = (ECPrivateKey) kp.getPrivate(); - publicKey = (ECPublicKey) kp.getPublic(); - } else if (parsed instanceof PrivateKeyInfo) { - // only private info → convert + derive public - privateKey = (ECPrivateKey) converter.getPrivateKey((PrivateKeyInfo) parsed); - - // curve parameters for P-256 / secp256r1 - ECNamedCurveParameterSpec ecP = ECNamedCurveTable.getParameterSpec("P-256"); - ECParameterSpec bcSpec = new ECNamedCurveParameterSpec("P-256", ecP.getCurve(), ecP.getG(), ecP.getN(), ecP.getH(), ecP.getSeed()); - - // do Q = d * G - BigInteger d = privateKey.getS(); - org.bouncycastle.math.ec.ECPoint Q = ecP.getG().multiply(d).normalize(); - java.security.spec.ECPoint w = new java.security.spec.ECPoint( - Q.getAffineXCoord().toBigInteger(), Q.getAffineYCoord().toBigInteger()); - - KeyFactory kf = KeyFactory.getInstance("EC", "BC"); - ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, bcSpec); - publicKey = (ECPublicKey) kf.generatePublic(pubSpec); - } else { - throw new IllegalArgumentException("Unrecognized PEM object: " + parsed.getClass()); - } - // 2. Build header & claims - long nowEpoch = Instant.now().getEpochSecond(); + KeyPair kp = loadECKeyPair(privateKeyPEM); + ECPublicKey publicKey = (ECPublicKey) kp.getPublic(); + ECPrivateKey privateKey = (ECPrivateKey) kp.getPrivate(); + + long now = Instant.now().getEpochSecond(); + + // you can drop your custom 'nonce' header if you prefer, + // or keep it if Coinbase wants it Map header = new HashMap<>(); header.put("alg", "ES256"); header.put("typ", "JWT"); - header.put("kid", name); - header.put("nonce", String.valueOf(nowEpoch)); + header.put("kid", keyId); + header.put("nonce", String.valueOf(now)); - Instant notBefore = Instant.ofEpochSecond(nowEpoch); - Instant expiresAt = notBefore.plusSeconds(120); - String uri = requestMethod + " " + url; + String uriClaim = method + " " + url; + + Date nbf = Date.from(Instant.ofEpochSecond(now)); + Date exp = Date.from(Instant.ofEpochSecond(now + 120)); - // 3. Sign with java-jwt Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); - return JWT.create().withHeader(header).withIssuer("cdp").withSubject(name) - .withNotBefore(Date.from(notBefore)).withExpiresAt(Date.from(expiresAt)) - .withClaim("uri", uri).sign(alg); + + return JWT.create().withHeader(header).withIssuer("cdp").withSubject(keyId).withNotBefore(nbf) + .withExpiresAt(exp).withClaim("uri", uriClaim).sign(alg); + } + + @Override + public String digestParams(RestInvocation restInvocation) { + final String pathWithQueryString = restInvocation.getInvocationUrl() + .replace(restInvocation.getBaseUrl(), ""); + final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP) + .toString(); + final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; + + String requestMethod = restInvocation.getHttpMethod(); + String url = restInvocation.getInvocationUrl(); + +// String jwt = generateJWT(restInvocation.getHttpMethod(), restInvocation.getInvocationUrl(), privateKeyPEM, keyId); + + return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); } } From 06deb9df95142f5bb8e9e458a36827a59ee5d185 Mon Sep 17 00:00:00 2001 From: David Pang Date: Tue, 29 Apr 2025 15:42:44 -0400 Subject: [PATCH 04/10] started v3 classes, restored v2 logic to original --- .../xchange/coinbase/v2/CoinbaseV2Digest.java | 121 -------------- .../knowm/xchange/coinbase/v3/Coinbase.java | 11 ++ .../coinbase/v3/CoinbaseAuthenticated.java | 143 +++++++++++++++++ .../xchange/coinbase/v3/CoinbaseExchange.java | 30 ++++ .../xchange/coinbase/v3/CoinbaseV3Digest.java | 151 ++++++++++++++++++ .../v3/dto/orders/CoinbaseOrderResponse.java | 5 + 6 files changed, 340 insertions(+), 121 deletions(-) create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseExchange.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java index 925cea4132b..50357d8c28f 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java @@ -2,40 +2,13 @@ import static org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated.CB_ACCESS_TIMESTAMP; -import com.auth0.jwt.JWT; -import com.auth0.jwt.algorithms.Algorithm; import jakarta.ws.rs.HeaderParam; -import java.io.IOException; -import java.io.StringReader; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.Security; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECFieldFp; -import java.time.Instant; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.jce.ECNamedCurveTable; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.knowm.xchange.service.BaseParamsDigest; import org.knowm.xchange.utils.DigestUtils; import si.mazi.rescu.RestInvocation; public class CoinbaseV2Digest extends BaseParamsDigest { - static { - // register BC once for PEM parsing + EC math - Security.addProvider(new BouncyCastleProvider()); - } - private CoinbaseV2Digest(String secretKey) { super(secretKey, HMAC_SHA_256); } @@ -44,95 +17,6 @@ public static CoinbaseV2Digest createInstance(String secretKey) { return secretKey == null ? null : new CoinbaseV2Digest(secretKey); } - /** - * Load an EC keypair from either a PEMKeyPair or a raw PrivateKeyInfo, deriving the public key on - * the P-256 curve if necessary. - */ - private static KeyPair loadECKeyPair(String privateKeyPEM) - throws IOException, GeneralSecurityException { - try (PEMParser parser = new PEMParser(new StringReader(privateKeyPEM))) { - Object obj = parser.readObject(); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); - - ECPrivateKey priv; - ECPublicKey pub; - - if (obj instanceof PEMKeyPair) { - KeyPair kp = converter.getKeyPair((PEMKeyPair) obj); - return kp; - } - - if (obj instanceof PrivateKeyInfo) { - // 1) get the private key - priv = (ECPrivateKey) converter.getPrivateKey((PrivateKeyInfo) obj); - - // 2) derive BC curve + base point - ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec("P-256"); - org.bouncycastle.math.ec.ECPoint Q = bcSpec.getG().multiply(priv.getS()).normalize(); - - // 3) convert BC point → JCA point - java.security.spec.ECPoint w = new java.security.spec.ECPoint( - Q.getAffineXCoord().toBigInteger(), Q.getAffineYCoord().toBigInteger()); - - // 4) build JCA curve spec from BC parameters - java.security.spec.EllipticCurve curve = new java.security.spec.EllipticCurve( - new ECFieldFp(bcSpec.getCurve().getField().getCharacteristic()), - bcSpec.getCurve().getA().toBigInteger(), bcSpec.getCurve().getB().toBigInteger(), - bcSpec.getSeed()); - java.security.spec.ECParameterSpec jcaSpec = new java.security.spec.ECParameterSpec(curve, - new java.security.spec.ECPoint(bcSpec.getG().getAffineXCoord().toBigInteger(), - bcSpec.getG().getAffineYCoord().toBigInteger()), bcSpec.getN(), - bcSpec.getH().intValue()); - - // 5) build the JCA public‐key spec and generate it - java.security.spec.ECPublicKeySpec pubSpec = new java.security.spec.ECPublicKeySpec(w, - jcaSpec); - KeyFactory kf = KeyFactory.getInstance("EC", "BC"); - pub = (ECPublicKey) kf.generatePublic(pubSpec); - - return new KeyPair(pub, priv); - } - - throw new IllegalArgumentException("Unknown PEM object: " + obj.getClass()); - } - } - - /** - * Generate an ES256 JWT for Coinbase Advanced Trading authentication. - * - * @param method HTTP method (e.g. "GET") - * @param url Full URL being called - * @param privateKeyPEM The PEM‐encoded EC private key (PKCS#8 or EC PRIVATE KEY) - * @param keyId Your API key ID (goes into the kid header, sub claim) - */ - public static String generateJWT(String method, String url, String privateKeyPEM, String keyId) - throws Exception { - - KeyPair kp = loadECKeyPair(privateKeyPEM); - ECPublicKey publicKey = (ECPublicKey) kp.getPublic(); - ECPrivateKey privateKey = (ECPrivateKey) kp.getPrivate(); - - long now = Instant.now().getEpochSecond(); - - // you can drop your custom 'nonce' header if you prefer, - // or keep it if Coinbase wants it - Map header = new HashMap<>(); - header.put("alg", "ES256"); - header.put("typ", "JWT"); - header.put("kid", keyId); - header.put("nonce", String.valueOf(now)); - - String uriClaim = method + " " + url; - - Date nbf = Date.from(Instant.ofEpochSecond(now)); - Date exp = Date.from(Instant.ofEpochSecond(now + 120)); - - Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); - - return JWT.create().withHeader(header).withIssuer("cdp").withSubject(keyId).withNotBefore(nbf) - .withExpiresAt(exp).withClaim("uri", uriClaim).sign(alg); - } - @Override public String digestParams(RestInvocation restInvocation) { final String pathWithQueryString = restInvocation.getInvocationUrl() @@ -141,11 +25,6 @@ public String digestParams(RestInvocation restInvocation) { .toString(); final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; - String requestMethod = restInvocation.getHttpMethod(); - String url = restInvocation.getInvocationUrl(); - -// String jwt = generateJWT(restInvocation.getHttpMethod(), restInvocation.getInvocationUrl(), privateKeyPEM, keyId); - return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); } } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java new file mode 100644 index 00000000000..2077686db05 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java @@ -0,0 +1,11 @@ +package org.knowm.xchange.coinbase.v3; + +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +@Produces(MediaType.APPLICATION_JSON) +public interface Coinbase extends org.knowm.xchange.coinbase.v2.Coinbase { + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java new file mode 100644 index 00000000000..45fc5558b93 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java @@ -0,0 +1,143 @@ +package org.knowm.xchange.coinbase.v3; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.Map; +import org.knowm.xchange.coinbase.v2.Coinbase; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountsData; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseBuyData; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseSellData; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; +import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseBuySellResponse; +import org.knowm.xchange.coinbase.v3.dto.orders.CoinbaseOrderResponse; +import si.mazi.rescu.ParamsDigest; + +@Path("/api/v3/brokerage") +@Produces(MediaType.APPLICATION_JSON) +public interface CoinbaseAuthenticated extends Coinbase { + + /** + * All Advanced Trade API requests must include an Authorization Bearer header containing a JSON + * Web Token (JWT) generated from the CDP API keys. + *

header = new HashMap<>(); + header.put("alg", "ES256"); + header.put("typ", "JWT"); + header.put("kid", keyId); + header.put("nonce", String.valueOf(now)); + + String uriClaim = method + " " + url; + + Date nbf = Date.from(Instant.ofEpochSecond(now)); + Date exp = Date.from(Instant.ofEpochSecond(now + 120)); + + Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); + + return JWT.create().withHeader(header).withIssuer("cdp").withSubject(keyId).withNotBefore(nbf) + .withExpiresAt(exp).withClaim("uri", uriClaim).sign(alg); + } + + @Override + public String digestParams(RestInvocation restInvocation) { + final String pathWithQueryString = restInvocation.getInvocationUrl() + .replace(restInvocation.getBaseUrl(), ""); + final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP) + .toString(); + final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; + + String requestMethod = restInvocation.getHttpMethod(); + String url = restInvocation.getInvocationUrl(); + +// String jwt = generateJWT(restInvocation.getHttpMethod(), restInvocation.getInvocationUrl(), privateKeyPEM, keyId); + + return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java new file mode 100644 index 00000000000..e40bdaf8d85 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java @@ -0,0 +1,5 @@ +package org.knowm.xchange.coinbase.v3.dto.orders; + +public class CoinbaseOrderResponse { + +} From 4db58e5a692d194382348acbb8fac33466e6097d Mon Sep 17 00:00:00 2001 From: David Pang Date: Fri, 16 May 2025 15:02:34 -0400 Subject: [PATCH 05/10] new utility class for handling JWT authentication --- .idea/codeStyles/Project.xml | 1 + .../coinbase/v2/CoinbaseAuthenticated.java | 11 +- .../v2/service/CoinbaseAccountServiceRaw.java | 63 +++----- .../coinbase/v3/CoinbaseAuthenticated.java | 71 +++++--- .../xchange/coinbase/v3/CoinbaseV3Digest.java | 63 ++++---- ...ponse.java => CoinbaseOrdersResponse.java} | 2 +- .../pricebook/CoinbasePriceBooksResponse.java | 5 + .../CoinbaseProductCandlesResponse.java | 5 + .../products/CoinbaseProductsResponse.java | 5 + .../v3/service/CoinbaseAccountService.java | 61 +++++++ .../v3/service/CoinbaseAccountServiceRaw.java | 152 ++++++++++++++++++ .../v3/service/CoinbaseBaseService.java | 74 +++++++++ .../AccountServiceIntegration.java | 3 +- .../BaseServiceIntegration.java | 3 +- .../MarketDataServiceIntegration.java | 3 +- .../TradeServiceIntegration.java | 4 +- .../v3/service/AccountServiceIntegration.java | 87 ++++++++++ 17 files changed, 512 insertions(+), 101 deletions(-) rename xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/{CoinbaseOrderResponse.java => CoinbaseOrdersResponse.java} (58%) create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java rename xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/{services => service}/AccountServiceIntegration.java (96%) rename xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/{services => service}/BaseServiceIntegration.java (89%) rename xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/{services => service}/MarketDataServiceIntegration.java (95%) rename xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/{services => service}/TradeServiceIntegration.java (95%) create mode 100644 xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 155ea5478db..4ebc02bbad4 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -49,6 +49,7 @@ developers.coinbase.com/api/v2#list-accounts + * href="https://developers.coinbase.com/api/v2#list-accounts">developers.coinbase.com/api/v2#list-accounts */ public List getCoinbaseAccounts() throws IOException { - String apiKey = exchange.getExchangeSpecification().getApiKey(); + String keyName = exchange.getExchangeSpecification().getApiKey(); + String secretKey = exchange.getExchangeSpecification().getSecretKey(); + + CoinbaseV3Digest authTokenGenerator = CoinbaseV3Digest.createInstance( + keyName, secretKey); List returnList = new ArrayList<>(); List tmpList = null; @@ -62,11 +67,7 @@ public List getCoinbaseAccounts() throws IOException { do { BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - tmpList = - coinbase - .getAccounts( - Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, 100, lastAccount) - .getData(); + tmpList = coinbase.getAccounts(authTokenGenerator, 100, lastAccount).getData(); lastAccount = null; if (tmpList != null && tmpList.size() > 0) { @@ -92,27 +93,21 @@ private boolean isValidUUID(String uuid) { * Authenticated resource that shows the current user account for the give currency. * * @see developers.coinbase.com/api/v2#show-an-account + * href="https://developers.coinbase.com/api/v2#show-an-account">developers.coinbase.com/api/v2#show-an-account */ public CoinbaseAccount getCoinbaseAccount(Currency currency) throws IOException { String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - return coinbase - .getAccount( - Coinbase.CB_VERSION_VALUE, - apiKey, - signatureCreator2, - timestamp, - currency.getCurrencyCode()) - .getData(); + return coinbase.getAccount(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, + currency.getCurrencyCode()).getData(); } /** * Authenticated resource that creates a new BTC account for the current user. * * @see developers.coinbase.com/api/v2#create-account + * href="https://developers.coinbase.com/api/v2#create-account">developers.coinbase.com/api/v2#create-account */ public CoinbaseAccount createCoinbaseAccount(String name) throws IOException { @@ -125,34 +120,28 @@ public CoinbaseAccount createCoinbaseAccount(String name) throws IOException { String signature = getSignature(timestamp, HttpMethod.POST, path, body); showCurl(HttpMethod.POST, apiKey, timestamp, signature, path, body); - return coinbase - .createAccount( - MediaType.APPLICATION_JSON, - Coinbase.CB_VERSION_VALUE, - apiKey, - signature, - timestamp, - payload) - .getData(); + return coinbase.createAccount(MediaType.APPLICATION_JSON, Coinbase.CB_VERSION_VALUE, apiKey, + signature, timestamp, payload).getData(); } /** * Authenticated resource that shows the current user payment methods. * * @see developers.coinbase.com/api/v2?shell#list-payment-methods + * href="https://developers.coinbase.com/api/v2#list-payment-methods">developers.coinbase.com/api/v2?shell#list-payment-methods */ public List getCoinbasePaymentMethods() throws IOException { String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - return coinbase - .getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp) - .getData(); + return coinbase.getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, + timestamp).getData(); } public static class CreateCoinbaseAccountPayload { - @JsonProperty String name; + + @JsonProperty + String name; CreateCoinbaseAccountPayload(String name) { this.name = name; diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java index 45fc5558b93..6a43526e7cd 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java @@ -10,16 +10,13 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.io.IOException; -import java.util.Map; import org.knowm.xchange.coinbase.v2.Coinbase; import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountsData; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseBuyData; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseSellData; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; -import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseBuySellResponse; -import org.knowm.xchange.coinbase.v3.dto.orders.CoinbaseOrderResponse; +import org.knowm.xchange.coinbase.v3.dto.orders.CoinbaseOrdersResponse; +import org.knowm.xchange.coinbase.v3.dto.pricebook.CoinbasePriceBooksResponse; +import org.knowm.xchange.coinbase.v3.dto.products.CoinbaseProductCandlesResponse; +import org.knowm.xchange.coinbase.v3.dto.products.CoinbaseProductsResponse; import si.mazi.rescu.ParamsDigest; @Path("/api/v3/brokerage") @@ -49,50 +46,88 @@ CoinbaseAccountsData listAccounts(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDiges @GET @Path("accounts/{account_id}") CoinbaseAccountsData getAccount(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, - @PathParam("account_id") String accountId) - throws IOException, CoinbaseException; + @PathParam("account_id") String accountId) throws IOException, CoinbaseException; @POST @Path("orders") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse createOrder(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + CoinbaseOrdersResponse createOrder(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, Object payload) throws IOException, CoinbaseException; @POST @Path("orders/batch_cancel") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse cancelOrders(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + CoinbaseOrdersResponse cancelOrders(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, Object payload) throws IOException, CoinbaseException; @GET @Path("orders/historical/batch") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse listOrders(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest) + CoinbaseOrdersResponse listOrders(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest) throws IOException, CoinbaseException; @GET @Path("orders/historical/fills") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse listFills(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest) + CoinbaseOrdersResponse listFills(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest) throws IOException, CoinbaseException; @GET @Path("orders/historical/{order_id}}") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse getOrder(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, - @PathParam("order_id") String orderId) - throws IOException, CoinbaseException; + CoinbaseOrdersResponse getOrder(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + @PathParam("order_id") String orderId) throws IOException, CoinbaseException; @POST @Path("orders/preview") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse previewOrders(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + CoinbaseOrdersResponse previewOrders(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, Object payload) throws IOException, CoinbaseException; @GET @Path("best_bid_ask") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseOrderResponse getBestBidAsk(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest) + CoinbasePriceBooksResponse getBestBidAsk( + @HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + @QueryParam("product_id") String productId) throws IOException, CoinbaseException; + + @GET + @Path("product_book") + @Consumes(MediaType.APPLICATION_JSON) + CoinbasePriceBooksResponse getProductBook( + @HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + @QueryParam("product_id") String productId, @QueryParam("limit") Integer limit, + @QueryParam("aggregation_price_increment") String aggregationPriceIncrement) + throws IOException, CoinbaseException; + + @GET + @Path("products") + @Consumes(MediaType.APPLICATION_JSON) + CoinbaseProductsResponse listProducts(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + @QueryParam("limit") Integer limit, @QueryParam("offset") Integer offset, + @QueryParam("product_type") String productType, + @QueryParam("product_ids") String[] productIds, + @QueryParam("contract_expiry_type") String contractExpiryType, + @QueryParam("expiring_contract_status") String expiringContractStatus, + @QueryParam("get_tradability_status") Boolean getTradabilityStatus, + @QueryParam("get_all_products") Boolean getAllProducts, + @QueryParam("products_sort_order") String productsSortOrder) + throws IOException, CoinbaseException; + + @GET + @Path("products/{product_id}") + @Consumes(MediaType.APPLICATION_JSON) + CoinbaseProductsResponse getProduct(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + @PathParam("product_id") String productId, + @QueryParam("get_tradability_status") Boolean getTradabilityStatus) + throws IOException, CoinbaseException; + + @GET + @Path("products/{product_id}/candles") + @Consumes(MediaType.APPLICATION_JSON) + CoinbaseProductCandlesResponse getProductCandles(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest jwtDigest, + @PathParam("product_id") String productId, + @QueryParam("get_tradability_status") Boolean getTradabilityStatus) throws IOException, CoinbaseException; // @GET diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java index f27f07d6931..1bff9980d4d 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java @@ -1,10 +1,7 @@ package org.knowm.xchange.coinbase.v3; -import static org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated.CB_ACCESS_TIMESTAMP; - import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; -import jakarta.ws.rs.HeaderParam; import java.io.IOException; import java.io.StringReader; import java.security.GeneralSecurityException; @@ -26,7 +23,6 @@ import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.knowm.xchange.service.BaseParamsDigest; -import org.knowm.xchange.utils.DigestUtils; import si.mazi.rescu.RestInvocation; public class CoinbaseV3Digest extends BaseParamsDigest { @@ -36,21 +32,27 @@ public class CoinbaseV3Digest extends BaseParamsDigest { Security.addProvider(new BouncyCastleProvider()); } - private CoinbaseV3Digest(String secretKey) { + private final String keyName; + private final String secretKey; + + private CoinbaseV3Digest(String keyName, String secretKey) { super(secretKey, HMAC_SHA_256); + + this.keyName = keyName; + this.secretKey = secretKey; } - public static CoinbaseV3Digest createInstance(String secretKey) { - return secretKey == null ? null : new CoinbaseV3Digest(secretKey); + public static CoinbaseV3Digest createInstance(String keyName, String secretKey) { + return keyName == null || secretKey == null ? null : new CoinbaseV3Digest(keyName, secretKey); } /** * Load an EC keypair from either a PEMKeyPair or a raw PrivateKeyInfo, deriving the public key on * the P-256 curve if necessary. */ - private static KeyPair loadECKeyPair(String privateKeyPEM) + private static KeyPair loadECKeyPair(String secretKey) throws IOException, GeneralSecurityException { - try (PEMParser parser = new PEMParser(new StringReader(privateKeyPEM))) { + try (PEMParser parser = new PEMParser(new StringReader(secretKey))) { Object obj = parser.readObject(); JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); @@ -100,52 +102,49 @@ private static KeyPair loadECKeyPair(String privateKeyPEM) /** * Generate an ES256 JWT for Coinbase Advanced Trading authentication. * - * @param method HTTP method (e.g. "GET") - * @param url Full URL being called - * @param privateKeyPEM The PEM‐encoded EC private key (PKCS#8 or EC PRIVATE KEY) - * @param keyId Your API key ID (goes into the kid header, sub claim) + * @param method HTTP method (e.g. "GET") + * @param uri Must be exactly host/path */ - public static String generateJWT(String method, String url, String privateKeyPEM, String keyId) - throws Exception { + private String generateJWT(String method, String uri) throws Exception { - KeyPair kp = loadECKeyPair(privateKeyPEM); + KeyPair kp = loadECKeyPair(secretKey); ECPublicKey publicKey = (ECPublicKey) kp.getPublic(); ECPrivateKey privateKey = (ECPrivateKey) kp.getPrivate(); long now = Instant.now().getEpochSecond(); - // you can drop your custom 'nonce' header if you prefer, - // or keep it if Coinbase wants it Map header = new HashMap<>(); header.put("alg", "ES256"); header.put("typ", "JWT"); - header.put("kid", keyId); + header.put("kid", keyName); header.put("nonce", String.valueOf(now)); - String uriClaim = method + " " + url; + String uriClaim = method + " " + uri; Date nbf = Date.from(Instant.ofEpochSecond(now)); Date exp = Date.from(Instant.ofEpochSecond(now + 120)); Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); - return JWT.create().withHeader(header).withIssuer("cdp").withSubject(keyId).withNotBefore(nbf) + return JWT.create().withHeader(header).withIssuer("cdp").withSubject(keyName).withNotBefore(nbf) .withExpiresAt(exp).withClaim("uri", uriClaim).sign(alg); } + private String generateAuthHeader(String method, String uri) { + String jwt = ""; + try { + jwt = generateJWT(method, uri); + } catch (Exception e) { + Coinbase.LOG.error("Exception generating JWT", e); + } + return "Bearer " + jwt; + } + @Override public String digestParams(RestInvocation restInvocation) { - final String pathWithQueryString = restInvocation.getInvocationUrl() - .replace(restInvocation.getBaseUrl(), ""); - final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP) - .toString(); - final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; - - String requestMethod = restInvocation.getHttpMethod(); - String url = restInvocation.getInvocationUrl(); - -// String jwt = generateJWT(restInvocation.getHttpMethod(), restInvocation.getInvocationUrl(), privateKeyPEM, keyId); + String method = restInvocation.getHttpMethod(); + String url = restInvocation.getBaseUrl().replace("https://", "") + restInvocation.getPath(); - return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); + return generateAuthHeader(method, url); } } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java similarity index 58% rename from xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java rename to xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java index e40bdaf8d85..09babfb6611 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrderResponse.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java @@ -1,5 +1,5 @@ package org.knowm.xchange.coinbase.v3.dto.orders; -public class CoinbaseOrderResponse { +public class CoinbaseOrdersResponse { } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java new file mode 100644 index 00000000000..5cec0e19c10 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java @@ -0,0 +1,5 @@ +package org.knowm.xchange.coinbase.v3.dto.pricebook; + +public class CoinbasePriceBooksResponse { + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java new file mode 100644 index 00000000000..119b132ffe1 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java @@ -0,0 +1,5 @@ +package org.knowm.xchange.coinbase.v3.dto.products; + +public class CoinbaseProductCandlesResponse { + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java new file mode 100644 index 00000000000..2dda06529b8 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java @@ -0,0 +1,5 @@ +package org.knowm.xchange.coinbase.v3.dto.products; + +public class CoinbaseProductsResponse { + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java new file mode 100644 index 00000000000..d64a10ea651 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java @@ -0,0 +1,61 @@ +package org.knowm.xchange.coinbase.v3.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.dto.account.AccountInfo; +import org.knowm.xchange.dto.account.Balance; +import org.knowm.xchange.dto.account.Wallet; +import org.knowm.xchange.exceptions.ExchangeException; +import org.knowm.xchange.exceptions.NotAvailableFromExchangeException; +import org.knowm.xchange.exceptions.NotYetImplementedForExchangeException; +import org.knowm.xchange.service.account.AccountService; +import org.knowm.xchange.service.trade.params.DefaultWithdrawFundsParams; +import org.knowm.xchange.service.trade.params.TradeHistoryParams; +import org.knowm.xchange.service.trade.params.WithdrawFundsParams; + +public final class CoinbaseAccountService extends CoinbaseAccountServiceRaw implements + AccountService { + + public CoinbaseAccountService(Exchange exchange) { + + super(exchange); + } + + @Override + public AccountInfo getAccountInfo() throws IOException { + List wallets = new ArrayList<>(); + + List coinbaseAccounts = getCoinbaseAccounts(); + for (CoinbaseAccountData.CoinbaseAccount coinbaseAccount : coinbaseAccounts) { + CoinbaseAmount balance = coinbaseAccount.getBalance(); + Wallet wallet = Wallet.Builder.from(Arrays.asList( + new Balance(Currency.getInstance(balance.getCurrency()), balance.getAmount()))) + .id(coinbaseAccount.getId()).build(); + wallets.add(wallet); + } + + return new AccountInfo(wallets); + } + + @Override + public String withdrawFunds(WithdrawFundsParams params) + throws ExchangeException, NotAvailableFromExchangeException, NotYetImplementedForExchangeException, IOException { + if (params instanceof DefaultWithdrawFundsParams) { + DefaultWithdrawFundsParams defaultParams = (DefaultWithdrawFundsParams) params; + return withdrawFunds(defaultParams.getCurrency(), defaultParams.getAmount(), + defaultParams.getAddress()); + } + throw new IllegalStateException("Don't know how to withdraw: " + params); + } + + @Override + public TradeHistoryParams createFundingHistoryParams() { + throw new NotAvailableFromExchangeException(); + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java new file mode 100644 index 00000000000..a6034782532 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java @@ -0,0 +1,152 @@ +package org.knowm.xchange.coinbase.v3.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; +import org.knowm.xchange.coinbase.v3.Coinbase; +import org.knowm.xchange.currency.Currency; + +public class CoinbaseAccountServiceRaw extends CoinbaseBaseService { + + public CoinbaseAccountServiceRaw(Exchange exchange) { + super(exchange); + } + +// public CoinbaseTransactionsResponse getTransactions(String accountId) throws IOException { +// String apiKey = exchange.getExchangeSpecification().getApiKey(); +// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() +// .getEpoch(); +// +// return coinbaseAdvancedTrade.getTransactions(Coinbase.CB_VERSION_VALUE, apiKey, +// authTokenCreator, timestamp, accountId); +// } +// +// public Map getDeposits(String accountId) throws IOException { +// String apiKey = exchange.getExchangeSpecification().getApiKey(); +// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() +// .getEpoch(); +// +// return coinbaseAdvancedTrade.getDeposits(Coinbase.CB_VERSION_VALUE, apiKey, authTokenCreator, +// timestamp, accountId); +// } +// +// public Map getWithdrawals(String accountId) throws IOException { +// String apiKey = exchange.getExchangeSpecification().getApiKey(); +// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() +// .getEpoch(); +// +// return coinbaseAdvancedTrade.getWithdrawals(Coinbase.CB_VERSION_VALUE, apiKey, +// authTokenCreator, timestamp, accountId); +// } + + /** + * Authenticated resource that shows the current user accounts. + * + * @see developers.coinbase.com/api/v2#list-accounts + */ + public List getCoinbaseAccounts() throws IOException { + String apiKey = exchange.getExchangeSpecification().getApiKey(); + + List returnList = new ArrayList<>(); + List tmpList = null; + + String lastAccount = null; + do { + BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() + .getEpoch(); + + tmpList = coinbaseAdvancedTrade.listAccounts(authTokenCreator, 100, lastAccount).getData(); + + lastAccount = null; + if (tmpList != null && tmpList.size() > 0) { + returnList.addAll(tmpList); + lastAccount = tmpList.get(tmpList.size() - 1).getId(); + } + + } while (lastAccount != null && isValidUUID(lastAccount)); + + return returnList; + } + + private boolean isValidUUID(String uuid) { + try { + UUID.fromString(uuid); + return true; + } catch (IllegalArgumentException exception) { + return false; + } + } + + /** + * Authenticated resource that shows the current user account for the give currency. + * + * @see developers.coinbase.com/api/v2#show-an-account + */ +// public CoinbaseAccount getCoinbaseAccount(Currency currency) throws IOException { +// String apiKey = exchange.getExchangeSpecification().getApiKey(); +// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() +// .getEpoch(); +// +// return coinbaseAdvancedTrade.getAccount(Coinbase.CB_VERSION_VALUE, apiKey, authTokenCreator, +// timestamp, currency.getCurrencyCode()).getData(); +// } + + /** + * Authenticated resource that creates a new BTC account for the current user. + * + * @see developers.coinbase.com/api/v2#create-account + */ +// public CoinbaseAccount createCoinbaseAccount(String name) throws IOException { +// +// CreateCoinbaseAccountPayload payload = new CreateCoinbaseAccountPayload(name); +// +// String path = "/v2/accounts"; +// String apiKey = exchange.getExchangeSpecification().getApiKey(); +// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() +// .getEpoch(); +// String body = new ObjectMapper().writeValueAsString(payload); +// String signature = getSignature(timestamp, HttpMethod.POST, path, body); +// showCurl(HttpMethod.POST, apiKey, timestamp, signature, path, body); +// +// return coinbaseAdvancedTrade.createAccount(MediaType.APPLICATION_JSON, +// Coinbase.CB_VERSION_VALUE, apiKey, signature, timestamp, payload).getData(); +// } + + /** + * Authenticated resource that shows the current user payment methods. + * + * @see developers.coinbase.com/api/v2?shell#list-payment-methods + */ +// public List getCoinbasePaymentMethods() throws IOException { +// String apiKey = exchange.getExchangeSpecification().getApiKey(); +// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() +// .getEpoch(); +// +// return coinbaseAdvancedTrade.getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, +// authTokenCreator, timestamp).getData(); +// } + + public static class CreateCoinbaseAccountPayload { + + @JsonProperty + String name; + + CreateCoinbaseAccountPayload(String name) { + this.name = name; + } + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java new file mode 100644 index 00000000000..8f7af489b56 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java @@ -0,0 +1,74 @@ +package org.knowm.xchange.coinbase.v3.service; + +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.client.ExchangeRestProxyBuilder; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseTimeData.CoinbaseTime; +import org.knowm.xchange.coinbase.v3.Coinbase; +import org.knowm.xchange.coinbase.v3.CoinbaseAuthenticated; +import org.knowm.xchange.coinbase.v3.CoinbaseV3Digest; +import org.knowm.xchange.service.BaseExchangeService; +import org.knowm.xchange.service.BaseService; +import si.mazi.rescu.ParamsDigest; + +public class CoinbaseBaseService extends BaseExchangeService implements BaseService { + + protected final CoinbaseAuthenticated coinbaseAdvancedTrade; + protected final ParamsDigest authTokenCreator; + + protected CoinbaseBaseService(Exchange exchange) { + + super(exchange); + coinbaseAdvancedTrade = ExchangeRestProxyBuilder.forInterface(CoinbaseAuthenticated.class, + exchange.getExchangeSpecification()).build(); + + authTokenCreator = CoinbaseV3Digest.createInstance( + exchange.getExchangeSpecification().getApiKey(), + exchange.getExchangeSpecification().getSecretKey()); + } + + /** + * Unauthenticated resource that returns currencies supported on Coinbase. + * + * @return A list of currency names and their corresponding ISO code. + * @see developers.coinbase.com/api/v2#get-currencies + */ + public List getCoinbaseCurrencies() throws IOException { + return coinbaseAdvancedTrade.getCurrencies(Coinbase.CB_VERSION_VALUE).getData(); + } + + /** + * Unauthenticated resource that tells you the server time. + * + * @return The current server time. + * @see developers.coinbase.com/api/v2#get-current-time + */ + public CoinbaseTime getCoinbaseTime() throws IOException { + return coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData(); + } + + protected void showCurl(HttpMethod method, String apiKey, BigDecimal timestamp, String signature, + String path, String json) { + String headers = String.format( + "-H 'CB-VERSION: 2017-11-26' -H 'CB-ACCESS-KEY: %s' -H 'CB-ACCESS-SIGN: %s' -H 'CB-ACCESS-TIMESTAMP: %s'", + apiKey, signature, timestamp); + if (method == HttpMethod.GET) { + Coinbase.LOG.debug(String.format("curl %s https://api.coinbase.com%s", headers, path)); + } else if (method == HttpMethod.POST) { + String payload = "-d '" + json + "'"; + Coinbase.LOG.debug( + String.format("curl -X %s -H 'Content-Type: %s' %s %s https://api.coinbase.com%s", method, + MediaType.APPLICATION_JSON, headers, payload, path)); + } + } + + public enum HttpMethod { + GET, POST + } +} diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java similarity index 96% rename from xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java rename to xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java index 13b5efafc61..5592b7b7c44 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/AccountServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java @@ -1,4 +1,4 @@ -package org.knowm.xchange.coinbase.v2.services; +package org.knowm.xchange.coinbase.v2.service; import java.util.List; import java.util.stream.Collectors; @@ -13,7 +13,6 @@ import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; -import org.knowm.xchange.coinbase.v2.service.CoinbaseAccountService; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.service.account.AccountService; import org.knowm.xchange.utils.AuthUtils; diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/BaseServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/BaseServiceIntegration.java similarity index 89% rename from xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/BaseServiceIntegration.java rename to xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/BaseServiceIntegration.java index e19b7ecd036..cd2ca1f8ecc 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/BaseServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/BaseServiceIntegration.java @@ -1,4 +1,4 @@ -package org.knowm.xchange.coinbase.v2.services; +package org.knowm.xchange.coinbase.v2.service; import static org.assertj.core.api.Assertions.assertThat; @@ -9,7 +9,6 @@ import org.knowm.xchange.ExchangeFactory; import org.knowm.xchange.coinbase.v2.CoinbaseExchange; import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseTimeData.CoinbaseTime; -import org.knowm.xchange.coinbase.v2.service.CoinbaseBaseService; public class BaseServiceIntegration { diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/MarketDataServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java similarity index 95% rename from xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/MarketDataServiceIntegration.java rename to xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java index f643dcd4e13..24a8a70e9c2 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/MarketDataServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java @@ -1,4 +1,4 @@ -package org.knowm.xchange.coinbase.v2.services; +package org.knowm.xchange.coinbase.v2.service; import static org.assertj.core.api.Assertions.assertThat; @@ -14,7 +14,6 @@ import org.knowm.xchange.coinbase.v2.CoinbaseExchange; import org.knowm.xchange.coinbase.v2.dto.CoinbasePrice; import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; -import org.knowm.xchange.coinbase.v2.service.CoinbaseMarketDataService; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.service.marketdata.MarketDataService; diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/TradeServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java similarity index 95% rename from xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/TradeServiceIntegration.java rename to xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java index 91db5ab9c44..5aed4c308d1 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/services/TradeServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java @@ -1,4 +1,4 @@ -package org.knowm.xchange.coinbase.v2.services; +package org.knowm.xchange.coinbase.v2.service; import java.io.IOException; import java.math.BigDecimal; @@ -13,8 +13,6 @@ import org.knowm.xchange.coinbase.v2.dto.CoinbasePrice; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseBuyData.CoinbaseBuy; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseSellData.CoinbaseSell; -import org.knowm.xchange.coinbase.v2.service.CoinbaseAccountService; -import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeService; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.service.trade.TradeService; import org.knowm.xchange.utils.AuthUtils; diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java new file mode 100644 index 00000000000..6c21ab17a57 --- /dev/null +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java @@ -0,0 +1,87 @@ +package org.knowm.xchange.coinbase.v3.service; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.coinbase.v3.CoinbaseExchange; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.service.account.AccountService; +import org.knowm.xchange.utils.AuthUtils; + +public class AccountServiceIntegration { + + static Exchange exchange; + static AccountService accountService; + + @BeforeClass + public static void beforeClass() { + ExchangeSpecification exchangeSpecification = ExchangeFactory.INSTANCE.createExchange(CoinbaseExchange.class).getDefaultExchangeSpecification(); + AuthUtils.setApiAndSecretKey(exchangeSpecification); + exchange = ExchangeFactory.INSTANCE.createExchange(exchangeSpecification); + accountService = exchange.getAccountService(); + } + + @Test + public void listAccounts() throws Exception { + + Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); + + CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; + List accounts = coinbaseService.getCoinbaseAccounts(); + Assert.assertTrue(accounts.size() > 0); + + CoinbaseAccount btcAccount = + accounts.stream() + .filter(t -> t.getName().equals("BTC Wallet")) + .collect(Collectors.toList()) + .get(0); + Assert.assertEquals("BTC", btcAccount.getBalance().getCurrency()); + Assert.assertEquals("BTC Wallet", btcAccount.getName()); + } + + @Test + public void getAccountByCurrency() throws Exception { + + Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); + + CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; +// CoinbaseAccount btcAccount = coinbaseService.getCoinbaseAccount(Currency.BTC); +// Assert.assertEquals("BTC", btcAccount.getBalance().getCurrency()); +// Assert.assertEquals("BTC Wallet", btcAccount.getName()); + } + + @Test + public void createAccount() throws Exception { + + Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); + + CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; + try { + //coinbaseService.createCoinbaseAccount("BTC Test"); + } catch (CoinbaseException ex) { + Assert.assertEquals(400, ex.getHttpStatusCode()); + Assert.assertEquals( + "Creation of multiple BTC accounts is not supported (HTTP status code: 400)", + ex.getMessage()); + } + } + + @Test + public void listPaymentMethods() throws Exception { + + Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); + + CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; +// List methods = coinbaseService.getCoinbasePaymentMethods(); +// Assert.assertTrue(methods.size() > 0); + } +} From a37f662820397dca4e2e01fff27a6b3cefc6a6ca Mon Sep 17 00:00:00 2001 From: David Pang Date: Fri, 16 May 2025 15:45:06 -0400 Subject: [PATCH 06/10] reorg and refine code --- .../xchange/coinbase/v3/CoinbaseV3Digest.java | 169 ++++++++++++++---- 1 file changed, 130 insertions(+), 39 deletions(-) diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java index 1bff9980d4d..4190d7b67ed 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java @@ -7,14 +7,14 @@ import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; +import java.security.SecureRandom; import java.security.Security; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.ECFieldFp; import java.time.Instant; +import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -25,31 +25,86 @@ import org.knowm.xchange.service.BaseParamsDigest; import si.mazi.rescu.RestInvocation; +/** + *

Generates ECDSA-signed JWT tokens for authenticating requests to the Coinbase Advanced + * Trading API. This class handles key pair initialization, JWT generation, and Bearer token + * construction for REST API requests.

+ * + *

Uses the BouncyCastle provider for elliptic curve cryptography operations (P-256 curve) and + * the Java Cryptographic Extension (JCE) for key pair derivation.

+ * + * @since 1.0 + */ + public class CoinbaseV3Digest extends BaseParamsDigest { + private static final SecureRandom RNG = new SecureRandom(); + static { - // register BC once for PEM parsing + EC math - Security.addProvider(new BouncyCastleProvider()); + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } } + private final ECPrivateKey privateKey; + private final ECPublicKey publicKey; private final String keyName; - private final String secretKey; - private CoinbaseV3Digest(String keyName, String secretKey) { + /** + *

Initializes a new digest instance with the provided API key name and secret key.

+ * + *

Performs the following actions:

+ *
    + *
  1. Registers the BouncyCastle security provider if not already present
  2. + *
  3. Parses and validates the EC key pair from the secret key PEM
  4. + *
  5. Stores the public/private keys and key name for JWT generation
  6. + *
+ * + * @param keyName API key name (identifies the key in Coinbase API) + * @param secretKey PEM-encoded private key string (ECDSA P-256 format) + * @throws Exception If key parsing fails or BouncyCastle provider initialization fails + * @see #createInstance(String, String) + */ + + private CoinbaseV3Digest(String keyName, String secretKey) throws Exception { super(secretKey, HMAC_SHA_256); + KeyPair kp = loadECKeyPair(normalizePem(secretKey)); + this.publicKey = (ECPublicKey) kp.getPublic(); + this.privateKey = (ECPrivateKey) kp.getPrivate(); this.keyName = keyName; - this.secretKey = secretKey; } + /** + * Creates a new instance of CoinbaseV3Digest. + * + * @param keyName API key name + * @param secretKey PEM-encoded private key string + * @return Initialized digest instance + * @throws IllegalStateException If key parsing fails or provider initialization fails + */ + public static CoinbaseV3Digest createInstance(String keyName, String secretKey) { - return keyName == null || secretKey == null ? null : new CoinbaseV3Digest(keyName, secretKey); + try { + return new CoinbaseV3Digest(keyName, secretKey); + } catch (Exception e) { + throw new IllegalStateException("Failed to initialize CoinbaseV3Digest", e); + } } /** - * Load an EC keypair from either a PEMKeyPair or a raw PrivateKeyInfo, deriving the public key on - * the P-256 curve if necessary. + *

Loads an EC key pair from PEM-encoded input, supporting both PEMKeyPair and PrivateKeyInfo + * formats.

+ * + *

For PrivateKeyInfo inputs, derives the public key using the P-256 curve parameters.

+ * + * @param secretKey PEM-encoded private key string + * @return KeyPair containing the EC private and public keys + * @throws IOException If parsing fails + * @throws GeneralSecurityException If key derivation or conversion fails + * @throws IllegalArgumentException If input format is unrecognized */ + private static KeyPair loadECKeyPair(String secretKey) throws IOException, GeneralSecurityException { try (PEMParser parser = new PEMParser(new StringReader(secretKey))) { @@ -100,51 +155,87 @@ private static KeyPair loadECKeyPair(String secretKey) } /** - * Generate an ES256 JWT for Coinbase Advanced Trading authentication. + * Generates a random hexadecimal string for use as a nonce in JWT headers. * - * @param method HTTP method (e.g. "GET") - * @param uri Must be exactly host/path + * @param bytes Number of bytes to generate (1 byte = 2 hex characters) + * @return Random hexadecimal string of length 2 * bytes */ - private String generateJWT(String method, String uri) throws Exception { - KeyPair kp = loadECKeyPair(secretKey); - ECPublicKey publicKey = (ECPublicKey) kp.getPublic(); - ECPrivateKey privateKey = (ECPrivateKey) kp.getPrivate(); + private static String randomHex(int bytes) { + byte[] buf = new byte[bytes]; + RNG.nextBytes(buf); + StringBuilder sb = new StringBuilder(bytes * 2); + for (byte b : buf) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } - long now = Instant.now().getEpochSecond(); + /** + * Normalizes PEM-formatted key strings by: + *
    + *
  • Replacing line breaks with standard newline characters
  • + *
  • Ensuring consistent header/footer formatting
  • + *
  • Removing extraneous whitespace
  • + *
+ * + * @param pem Raw PEM string (may contain escaped newlines) + * @return Normalized PEM string ready for parsing + */ - Map header = new HashMap<>(); - header.put("alg", "ES256"); - header.put("typ", "JWT"); - header.put("kid", keyName); - header.put("nonce", String.valueOf(now)); + private static String normalizePem(String pem) { + return pem.replace("\\n", "\n") + .replaceAll("-----BEGIN (.*) KEY-----\\s+", "-----BEGIN $1 KEY-----\n") + .replaceAll("\\s+\n-----END (.*) KEY-----", "-----END $1 KEY-----"); + } - String uriClaim = method + " " + uri; + /** + *

Generates a JWT token for Coinbase Advanced Trading API authentication using the ES256 + * algorithm.

+ * + *

Includes the following claims:

+ *
    + *
  • {@code kid}: API key name
  • + *
  • {@code iss}: "cdp" (issuer)
  • + *
  • {@code sub}: API key name
  • + *
  • {@code nbf}: Not before timestamp (current time)
  • + *
  • {@code exp}: Expiration timestamp (2 minutes from now)
  • + *
  • {@code uri}: Concatenation of HTTP method and URI path
  • + *
  • {@code nonce}: Random 16-byte hex value
  • + *
+ * + * @param method HTTP method (e.g., "GET", "POST") + * @param uri Host/path combination (e.g., "api.coinbase.com/v3/brokerage/orders") + * @return Signed JWT string + */ + private String generateJWT(String method, String uri) { + long now = Instant.now().getEpochSecond(); Date nbf = Date.from(Instant.ofEpochSecond(now)); Date exp = Date.from(Instant.ofEpochSecond(now + 120)); Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); - return JWT.create().withHeader(header).withIssuer("cdp").withSubject(keyName).withNotBefore(nbf) - .withExpiresAt(exp).withClaim("uri", uriClaim).sign(alg); + return JWT.create().withKeyId(keyName).withIssuer("cdp").withSubject(keyName).withNotBefore(nbf) + .withExpiresAt(exp).withClaim("uri", method + " " + uri) + .withHeader(Collections.singletonMap("nonce", randomHex(16))).sign(alg); } - private String generateAuthHeader(String method, String uri) { - String jwt = ""; - try { - jwt = generateJWT(method, uri); - } catch (Exception e) { - Coinbase.LOG.error("Exception generating JWT", e); - } - return "Bearer " + jwt; - } + /** + *

Generates the Bearer authentication header for REST API requests.

+ * + *

Constructs a JWT token containing the request method and path, then formats it as a Bearer + * token:

+ *
"Bearer " + JWT_TOKEN
+ * + * @param restInvocation Rescu {@link RestInvocation} object containing request metadata + * @return Bearer token string for authentication header + */ @Override public String digestParams(RestInvocation restInvocation) { - String method = restInvocation.getHttpMethod(); - String url = restInvocation.getBaseUrl().replace("https://", "") + restInvocation.getPath(); - - return generateAuthHeader(method, url); + String hostAndPath = + restInvocation.getBaseUrl().replaceFirst("^https?://", "") + restInvocation.getPath(); + return "Bearer " + generateJWT(restInvocation.getHttpMethod(), hostAndPath); } } From f40dc18c8dd3d276c0126c9102eb74dc6624382f Mon Sep 17 00:00:00 2001 From: David Pang Date: Mon, 19 May 2025 11:18:36 -0400 Subject: [PATCH 07/10] removed deprecated headers --- .../knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java index ade3054d53c..4a58e9c1b63 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java @@ -109,11 +109,7 @@ Map getWithdrawals( @GET @Path("accounts") CoinbaseAccountsData getAccounts( -// @HeaderParam(CB_VERSION) String apiVersion, -// @HeaderParam(CB_ACCESS_KEY) String apiKey, -// @HeaderParam(CB_ACCESS_SIGN) ParamsDigest signature, -// @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest signature, + @HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest authTokenGenerator, @QueryParam("limit") Integer limit, @QueryParam("starting_after") String starting_after) throws IOException, CoinbaseException; From e6fbfdb5e40e18903a44278063364738b4383112 Mon Sep 17 00:00:00 2001 From: David Pang Date: Mon, 19 May 2025 12:05:09 -0400 Subject: [PATCH 08/10] WIP create account --- .../coinbase/v2/CoinbaseAuthenticated.java | 88 ++----- .../xchange/coinbase/v2/CoinbaseExchange.java | 4 + .../xchange/coinbase/v2/CoinbaseV2Digest.java | 241 +++++++++++++++++- .../v2/service/CoinbaseAccountServiceRaw.java | 23 +- .../v2/service/CoinbaseBaseService.java | 6 +- .../v2/service/CoinbaseTradeService.java | 4 +- .../knowm/xchange/coinbase/v3/Coinbase.java | 11 - .../coinbase/v3/CoinbaseAuthenticated.java | 178 ------------- .../xchange/coinbase/v3/CoinbaseExchange.java | 30 --- .../xchange/coinbase/v3/CoinbaseV3Digest.java | 241 ------------------ .../v3/dto/orders/CoinbaseOrdersResponse.java | 5 - .../pricebook/CoinbasePriceBooksResponse.java | 5 - .../CoinbaseProductCandlesResponse.java | 5 - .../products/CoinbaseProductsResponse.java | 5 - .../v3/service/CoinbaseAccountService.java | 61 ----- .../v3/service/CoinbaseAccountServiceRaw.java | 152 ----------- .../v3/service/CoinbaseBaseService.java | 74 ------ .../v3/service/AccountServiceIntegration.java | 87 ------- 18 files changed, 270 insertions(+), 950 deletions(-) delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseExchange.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseV3Digest.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java delete mode 100644 xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java index 4a58e9c1b63..4fe097e667c 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java @@ -20,7 +20,6 @@ import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseSellData; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseBuySellResponse; -import org.knowm.xchange.coinbase.v3.CoinbaseV3Digest; import si.mazi.rescu.ParamsDigest; @Path("/v2") @@ -32,8 +31,8 @@ public interface CoinbaseAuthenticated extends Coinbase { * *

All request bodies should have content type application/json and be valid JSON. * - *

The CB-ACCESS-SIGN header is generated by creating a sha256 HMAC using the secret key on the - * prehash string timestamp + method + requestPath + body (where + represents string + *

The CB-ACCESS-SIGN header is generated by creating a sha256 HMAC using the secret key on + * the prehash string timestamp + method + requestPath + body (where + represents string * concatenation). The timestamp value is the same as the CB-ACCESS-TIMESTAMP header. * *

The body is the request body string or omitted if there is no request body (typically for @@ -54,120 +53,89 @@ public interface CoinbaseAuthenticated extends Coinbase { @GET @Path("accounts/{accountId}/transactions") - CoinbaseTransactionsResponse getTransactions( - @HeaderParam(CB_VERSION) String apiVersion, + CoinbaseTransactionsResponse getTransactions(@HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) ParamsDigest signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("accountId") String accountId) - throws IOException, CoinbaseException; + @PathParam("accountId") String accountId) throws IOException, CoinbaseException; @GET @Path("accounts/{accountId}/buys") - CoinbaseBuySellResponse getBuys( - @HeaderParam(CB_VERSION) String apiVersion, + CoinbaseBuySellResponse getBuys(@HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) CoinbaseV2Digest signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("accountId") String accountId, - @QueryParam("limit") Integer limit, - @QueryParam("starting_after") String startingAfter) - throws IOException, CoinbaseException; + @PathParam("accountId") String accountId, @QueryParam("limit") Integer limit, + @QueryParam("starting_after") String startingAfter) throws IOException, CoinbaseException; @GET @Path("accounts/{accountId}/sells") - CoinbaseBuySellResponse getSells( - @HeaderParam(CB_VERSION) String apiVersion, + CoinbaseBuySellResponse getSells(@HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) CoinbaseV2Digest signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("accountId") String accountId, - @QueryParam("limit") Integer limit, - @QueryParam("starting_after") String startingAfter) - throws IOException, CoinbaseException; + @PathParam("accountId") String accountId, @QueryParam("limit") Integer limit, + @QueryParam("starting_after") String startingAfter) throws IOException, CoinbaseException; @GET @Path("accounts/{accountId}/deposits") - Map getDeposits( - @HeaderParam(CB_VERSION) String apiVersion, + Map getDeposits(@HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) ParamsDigest signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("accountId") String accountId) - throws IOException, CoinbaseException; + @PathParam("accountId") String accountId) throws IOException, CoinbaseException; @GET @Path("accounts/{accountId}/withdrawals") - Map getWithdrawals( - @HeaderParam(CB_VERSION) String apiVersion, + Map getWithdrawals(@HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) ParamsDigest signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("accountId") String accountId) - throws IOException, CoinbaseException; + @PathParam("accountId") String accountId) throws IOException, CoinbaseException; @GET @Path("accounts") CoinbaseAccountsData getAccounts( @HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest authTokenGenerator, - @QueryParam("limit") Integer limit, - @QueryParam("starting_after") String starting_after) + @QueryParam("limit") Integer limit, @QueryParam("starting_after") String starting_after) throws IOException, CoinbaseException; @GET @Path("accounts/{currency}") - CoinbaseAccountData getAccount( - @HeaderParam(CB_VERSION) String apiVersion, - @HeaderParam(CB_ACCESS_KEY) String apiKey, - @HeaderParam(CB_ACCESS_SIGN) CoinbaseV2Digest signature, - @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("currency") String currency) - throws IOException, CoinbaseException; + CoinbaseAccountData getAccount(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest authTokenGenerator, + @PathParam("currency") String currency) throws IOException, CoinbaseException; @POST @Path("accounts") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseAccountData createAccount( - @HeaderParam(CONTENT_TYPE) String contentType, - @HeaderParam(CB_VERSION) String apiVersion, - @HeaderParam(CB_ACCESS_KEY) String apiKey, + CoinbaseAccountData createAccount(@HeaderParam(CONTENT_TYPE) String contentType, + @HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) String signature, - @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - Object payload) + @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, Object payload) throws IOException, CoinbaseException; @GET @Path("payment-methods") - CoinbasePaymentMethodsData getPaymentMethods( - @HeaderParam(CB_VERSION) String apiVersion, + CoinbasePaymentMethodsData getPaymentMethods(@HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) CoinbaseV2Digest signature, - @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp) - throws IOException, CoinbaseException; + @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp) throws IOException, CoinbaseException; @POST @Path("accounts/{account}/buys") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseBuyData buy( - @HeaderParam(CONTENT_TYPE) String contentType, - @HeaderParam(CB_VERSION) String apiVersion, - @HeaderParam(CB_ACCESS_KEY) String apiKey, + CoinbaseBuyData buy(@HeaderParam(CONTENT_TYPE) String contentType, + @HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) String signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("account") String accountId, - Object payload) - throws IOException, CoinbaseException; + @PathParam("account") String accountId, Object payload) throws IOException, CoinbaseException; @POST @Path("accounts/{account}/sells") @Consumes(MediaType.APPLICATION_JSON) - CoinbaseSellData sell( - @HeaderParam(CONTENT_TYPE) String contentType, - @HeaderParam(CB_VERSION) String apiVersion, - @HeaderParam(CB_ACCESS_KEY) String apiKey, + CoinbaseSellData sell(@HeaderParam(CONTENT_TYPE) String contentType, + @HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, @HeaderParam(CB_ACCESS_SIGN) String signature, @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, - @PathParam("account") String accountId, - Object payload) - throws IOException, CoinbaseException; + @PathParam("account") String accountId, Object payload) throws IOException, CoinbaseException; } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseExchange.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseExchange.java index 7c95f4e19bd..b56cad6050f 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseExchange.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseExchange.java @@ -6,6 +6,10 @@ import org.knowm.xchange.coinbase.v2.service.CoinbaseAccountService; import org.knowm.xchange.coinbase.v2.service.CoinbaseMarketDataService; import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeService; +import org.knowm.xchange.service.account.AccountService; +import org.knowm.xchange.service.marketdata.MarketDataService; +import org.knowm.xchange.service.trade.TradeService; +import org.knowm.xchange.utils.AuthUtils; public class CoinbaseExchange extends BaseExchange implements Exchange { diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java index 50357d8c28f..41c125b1a9f 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseV2Digest.java @@ -1,30 +1,243 @@ package org.knowm.xchange.coinbase.v2; -import static org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated.CB_ACCESS_TIMESTAMP; - -import jakarta.ws.rs.HeaderParam; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import java.io.IOException; +import java.io.StringReader; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.security.Security; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.knowm.xchange.service.BaseParamsDigest; -import org.knowm.xchange.utils.DigestUtils; import si.mazi.rescu.RestInvocation; +/** + *

Generates ECDSA-signed JWT tokens for authenticating requests to the Coinbase Advanced + * Trading API. This class handles key pair initialization, JWT generation, and Bearer token + * construction for REST API requests.

+ * + *

Uses the BouncyCastle provider for elliptic curve cryptography operations (P-256 curve) and + * the Java Cryptographic Extension (JCE) for key pair derivation.

+ * + * @since 1.0 + */ public class CoinbaseV2Digest extends BaseParamsDigest { - private CoinbaseV2Digest(String secretKey) { + private static final SecureRandom RNG = new SecureRandom(); + + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + private final ECPrivateKey privateKey; + private final ECPublicKey publicKey; + private final String keyName; + + /** + *

Initializes a new digest instance with the provided API key name and secret key.

+ * + *

Performs the following actions:

+ *
    + *
  1. Registers the BouncyCastle security provider if not already present
  2. + *
  3. Parses and validates the EC key pair from the secret key PEM
  4. + *
  5. Stores the public/private keys and key name for JWT generation
  6. + *
+ * + * @param keyName API key name (identifies the key in Coinbase API) + * @param secretKey PEM-encoded private key string (ECDSA P-256 format) + * @throws Exception If key parsing fails or BouncyCastle provider initialization fails + * @see #createInstance(String, String) + */ + + private CoinbaseV2Digest(String keyName, String secretKey) throws Exception { super(secretKey, HMAC_SHA_256); + + KeyPair kp = loadECKeyPair(normalizePem(secretKey)); + this.publicKey = (ECPublicKey) kp.getPublic(); + this.privateKey = (ECPrivateKey) kp.getPrivate(); + this.keyName = keyName; } - public static CoinbaseV2Digest createInstance(String secretKey) { - return secretKey == null ? null : new CoinbaseV2Digest(secretKey); + /** + * Creates a new instance of CoinbaseV3Digest. + * + * @param keyName API key name + * @param secretKey PEM-encoded private key string + * @return Initialized digest instance + * @throws IllegalStateException If key parsing fails or provider initialization fails + */ + + public static CoinbaseV2Digest createInstance(String keyName, String secretKey) { + if (keyName == null || secretKey == null) { + return null; + } + try { + return new CoinbaseV2Digest(keyName, secretKey); + } catch (Exception e) { + throw new IllegalStateException("Failed to initialize CoinbaseV2Digest", e); + } } + /** + *

Loads an EC key pair from PEM-encoded input, supporting both PEMKeyPair and PrivateKeyInfo + * formats.

+ * + *

For PrivateKeyInfo inputs, derives the public key using the P-256 curve parameters.

+ * + * @param secretKey PEM-encoded private key string + * @return KeyPair containing the EC private and public keys + * @throws IOException If parsing fails + * @throws GeneralSecurityException If key derivation or conversion fails + * @throws IllegalArgumentException If input format is unrecognized + */ + + private static KeyPair loadECKeyPair(String secretKey) + throws IOException, GeneralSecurityException { + try (PEMParser parser = new PEMParser(new StringReader(secretKey))) { + Object obj = parser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + + ECPrivateKey priv; + ECPublicKey pub; + + if (obj instanceof PEMKeyPair) { + KeyPair kp = converter.getKeyPair((PEMKeyPair) obj); + return kp; + } + + if (obj instanceof PrivateKeyInfo) { + // 1) get the private key + priv = (ECPrivateKey) converter.getPrivateKey((PrivateKeyInfo) obj); + + // 2) derive BC curve + base point + ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec("P-256"); + org.bouncycastle.math.ec.ECPoint Q = bcSpec.getG().multiply(priv.getS()).normalize(); + + // 3) convert BC point → JCA point + java.security.spec.ECPoint w = new java.security.spec.ECPoint( + Q.getAffineXCoord().toBigInteger(), Q.getAffineYCoord().toBigInteger()); + + // 4) build JCA curve spec from BC parameters + java.security.spec.EllipticCurve curve = new java.security.spec.EllipticCurve( + new ECFieldFp(bcSpec.getCurve().getField().getCharacteristic()), + bcSpec.getCurve().getA().toBigInteger(), bcSpec.getCurve().getB().toBigInteger(), + bcSpec.getSeed()); + java.security.spec.ECParameterSpec jcaSpec = new java.security.spec.ECParameterSpec(curve, + new java.security.spec.ECPoint(bcSpec.getG().getAffineXCoord().toBigInteger(), + bcSpec.getG().getAffineYCoord().toBigInteger()), bcSpec.getN(), + bcSpec.getH().intValue()); + + // 5) build the JCA public‐key spec and generate it + java.security.spec.ECPublicKeySpec pubSpec = new java.security.spec.ECPublicKeySpec(w, + jcaSpec); + KeyFactory kf = KeyFactory.getInstance("EC", "BC"); + pub = (ECPublicKey) kf.generatePublic(pubSpec); + + return new KeyPair(pub, priv); + } + + throw new IllegalArgumentException("Unknown PEM object: " + obj.getClass()); + } + } + + /** + * Generates a random hexadecimal string for use as a nonce in JWT headers. + * + * @param bytes Number of bytes to generate (1 byte = 2 hex characters) + * @return Random hexadecimal string of length 2 * bytes + */ + + private static String randomHex(int bytes) { + byte[] buf = new byte[bytes]; + RNG.nextBytes(buf); + StringBuilder sb = new StringBuilder(bytes * 2); + for (byte b : buf) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * Normalizes PEM-formatted key strings by: + *
    + *
  • Replacing line breaks with standard newline characters
  • + *
  • Ensuring consistent header/footer formatting
  • + *
  • Removing extraneous whitespace
  • + *
+ * + * @param pem Raw PEM string (may contain escaped newlines) + * @return Normalized PEM string ready for parsing + */ + + private static String normalizePem(String pem) { + return pem.replace("\\n", "\n") + .replaceAll("-----BEGIN (.*) KEY-----\\s+", "-----BEGIN $1 KEY-----\n") + .replaceAll("\\s+\n-----END (.*) KEY-----", "-----END $1 KEY-----"); + } + + /** + *

Generates a JWT token for Coinbase Advanced Trading API authentication using the ES256 + * algorithm.

+ * + *

Includes the following claims:

+ *
    + *
  • {@code kid}: API key name
  • + *
  • {@code iss}: "cdp" (issuer)
  • + *
  • {@code sub}: API key name
  • + *
  • {@code nbf}: Not before timestamp (current time)
  • + *
  • {@code exp}: Expiration timestamp (2 minutes from now)
  • + *
  • {@code uri}: Concatenation of HTTP method and URI path
  • + *
  • {@code nonce}: Random 16-byte hex value
  • + *
+ * + * @param method HTTP method (e.g., "GET", "POST") + * @param uri Host/path combination (e.g., "api.coinbase.com/v3/brokerage/orders") + * @return Signed JWT string + */ + + private String generateJWT(String method, String uri) { + long now = Instant.now().getEpochSecond(); + Date nbf = Date.from(Instant.ofEpochSecond(now)); + Date exp = Date.from(Instant.ofEpochSecond(now + 120)); + + Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); + + return JWT.create().withKeyId(keyName).withIssuer("cdp").withSubject(keyName).withNotBefore(nbf) + .withExpiresAt(exp).withClaim("uri", method + " " + uri) + .withHeader(Collections.singletonMap("nonce", randomHex(16))).sign(alg); + } + + /** + *

Generates the Bearer authentication header for REST API requests.

+ * + *

Constructs a JWT token containing the request method and path, then formats it as a Bearer + * token:

+ *
"Bearer " + JWT_TOKEN
+ * + * @param restInvocation Rescu {@link RestInvocation} object containing request metadata + * @return Bearer token string for authentication header + */ + @Override public String digestParams(RestInvocation restInvocation) { - final String pathWithQueryString = restInvocation.getInvocationUrl() - .replace(restInvocation.getBaseUrl(), ""); - final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP) - .toString(); - final String message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; - - return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); + String hostAndPath = + restInvocation.getBaseUrl().replaceFirst("^https?://", "") + restInvocation.getPath(); + return "Bearer " + generateJWT(restInvocation.getHttpMethod(), hostAndPath); } } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java index 0309cd120c6..073e93929c6 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java @@ -14,7 +14,6 @@ import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; -import org.knowm.xchange.coinbase.v3.CoinbaseV3Digest; import org.knowm.xchange.currency.Currency; public class CoinbaseAccountServiceRaw extends CoinbaseBaseService { @@ -27,15 +26,15 @@ public CoinbaseTransactionsResponse getTransactions(String accountId) throws IOE String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - return coinbase.getTransactions(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, - accountId); + return coinbase.getTransactions(Coinbase.CB_VERSION_VALUE, apiKey, authTokenGenerator, + timestamp, accountId); } public Map getDeposits(String accountId) throws IOException { String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - return coinbase.getDeposits(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, + return coinbase.getDeposits(Coinbase.CB_VERSION_VALUE, apiKey, authTokenGenerator, timestamp, accountId); } @@ -43,7 +42,7 @@ public Map getWithdrawals(String accountId) throws IOException { String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - return coinbase.getWithdrawals(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, + return coinbase.getWithdrawals(Coinbase.CB_VERSION_VALUE, apiKey, authTokenGenerator, timestamp, accountId); } @@ -54,12 +53,6 @@ public Map getWithdrawals(String accountId) throws IOException { * href="https://developers.coinbase.com/api/v2#list-accounts">developers.coinbase.com/api/v2#list-accounts */ public List getCoinbaseAccounts() throws IOException { - String keyName = exchange.getExchangeSpecification().getApiKey(); - String secretKey = exchange.getExchangeSpecification().getSecretKey(); - - CoinbaseV3Digest authTokenGenerator = CoinbaseV3Digest.createInstance( - keyName, secretKey); - List returnList = new ArrayList<>(); List tmpList = null; @@ -96,11 +89,7 @@ private boolean isValidUUID(String uuid) { * href="https://developers.coinbase.com/api/v2#show-an-account">developers.coinbase.com/api/v2#show-an-account */ public CoinbaseAccount getCoinbaseAccount(Currency currency) throws IOException { - String apiKey = exchange.getExchangeSpecification().getApiKey(); - BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - - return coinbase.getAccount(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, - currency.getCurrencyCode()).getData(); + return coinbase.getAccount(authTokenGenerator, currency.getCurrencyCode()).getData(); } /** @@ -134,7 +123,7 @@ public List getCoinbasePaymentMethods() throws IOExceptio String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - return coinbase.getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, + return coinbase.getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, authTokenGenerator, timestamp).getData(); } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java index 28b540fd5fd..7379923be2b 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java @@ -21,7 +21,7 @@ public class CoinbaseBaseService extends BaseExchangeService implements BaseService { protected final CoinbaseAuthenticated coinbase; - protected final CoinbaseV2Digest signatureCreator2; + protected final CoinbaseV2Digest authTokenGenerator; protected CoinbaseBaseService(Exchange exchange) { @@ -31,8 +31,8 @@ protected CoinbaseBaseService(Exchange exchange) { CoinbaseAuthenticated.class, exchange.getExchangeSpecification()) .build(); - signatureCreator2 = - CoinbaseV2Digest.createInstance(exchange.getExchangeSpecification().getSecretKey()); + authTokenGenerator = + CoinbaseV2Digest.createInstance(exchange.getExchangeSpecification().getApiKey(), exchange.getExchangeSpecification().getSecretKey()); } /** diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeService.java index ab7345c1fd3..7531a7507e2 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeService.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeService.java @@ -91,7 +91,7 @@ public UserTrades getBuyTradeHistory(CoinbaseTradeHistoryParams params, String a coinbase.getBuys( Coinbase.CB_VERSION_VALUE, apiKey, - signatureCreator2, + authTokenGenerator, timestamp, accountId, params.getLimit(), @@ -111,7 +111,7 @@ public UserTrades getSellTradeHistory(CoinbaseTradeHistoryParams params, String coinbase.getSells( Coinbase.CB_VERSION_VALUE, apiKey, - signatureCreator2, + authTokenGenerator, timestamp, accountId, params.getLimit(), diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java deleted file mode 100644 index 2077686db05..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/Coinbase.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.knowm.xchange.coinbase.v3; - -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -@Path("/") -@Produces(MediaType.APPLICATION_JSON) -public interface Coinbase extends org.knowm.xchange.coinbase.v2.Coinbase { - -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java deleted file mode 100644 index 6a43526e7cd..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticated.java +++ /dev/null @@ -1,178 +0,0 @@ -package org.knowm.xchange.coinbase.v3; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import org.knowm.xchange.coinbase.v2.Coinbase; -import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountsData; -import org.knowm.xchange.coinbase.v3.dto.orders.CoinbaseOrdersResponse; -import org.knowm.xchange.coinbase.v3.dto.pricebook.CoinbasePriceBooksResponse; -import org.knowm.xchange.coinbase.v3.dto.products.CoinbaseProductCandlesResponse; -import org.knowm.xchange.coinbase.v3.dto.products.CoinbaseProductsResponse; -import si.mazi.rescu.ParamsDigest; - -@Path("/api/v3/brokerage") -@Produces(MediaType.APPLICATION_JSON) -public interface CoinbaseAuthenticated extends Coinbase { - - /** - * All Advanced Trade API requests must include an Authorization Bearer header containing a JSON - * Web Token (JWT) generated from the CDP API keys. - *

Generates ECDSA-signed JWT tokens for authenticating requests to the Coinbase Advanced - * Trading API. This class handles key pair initialization, JWT generation, and Bearer token - * construction for REST API requests.

- * - *

Uses the BouncyCastle provider for elliptic curve cryptography operations (P-256 curve) and - * the Java Cryptographic Extension (JCE) for key pair derivation.

- * - * @since 1.0 - */ - -public class CoinbaseV3Digest extends BaseParamsDigest { - - private static final SecureRandom RNG = new SecureRandom(); - - static { - if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { - Security.addProvider(new BouncyCastleProvider()); - } - } - - private final ECPrivateKey privateKey; - private final ECPublicKey publicKey; - private final String keyName; - - /** - *

Initializes a new digest instance with the provided API key name and secret key.

- * - *

Performs the following actions:

- *
    - *
  1. Registers the BouncyCastle security provider if not already present
  2. - *
  3. Parses and validates the EC key pair from the secret key PEM
  4. - *
  5. Stores the public/private keys and key name for JWT generation
  6. - *
- * - * @param keyName API key name (identifies the key in Coinbase API) - * @param secretKey PEM-encoded private key string (ECDSA P-256 format) - * @throws Exception If key parsing fails or BouncyCastle provider initialization fails - * @see #createInstance(String, String) - */ - - private CoinbaseV3Digest(String keyName, String secretKey) throws Exception { - super(secretKey, HMAC_SHA_256); - - KeyPair kp = loadECKeyPair(normalizePem(secretKey)); - this.publicKey = (ECPublicKey) kp.getPublic(); - this.privateKey = (ECPrivateKey) kp.getPrivate(); - this.keyName = keyName; - } - - /** - * Creates a new instance of CoinbaseV3Digest. - * - * @param keyName API key name - * @param secretKey PEM-encoded private key string - * @return Initialized digest instance - * @throws IllegalStateException If key parsing fails or provider initialization fails - */ - - public static CoinbaseV3Digest createInstance(String keyName, String secretKey) { - try { - return new CoinbaseV3Digest(keyName, secretKey); - } catch (Exception e) { - throw new IllegalStateException("Failed to initialize CoinbaseV3Digest", e); - } - } - - /** - *

Loads an EC key pair from PEM-encoded input, supporting both PEMKeyPair and PrivateKeyInfo - * formats.

- * - *

For PrivateKeyInfo inputs, derives the public key using the P-256 curve parameters.

- * - * @param secretKey PEM-encoded private key string - * @return KeyPair containing the EC private and public keys - * @throws IOException If parsing fails - * @throws GeneralSecurityException If key derivation or conversion fails - * @throws IllegalArgumentException If input format is unrecognized - */ - - private static KeyPair loadECKeyPair(String secretKey) - throws IOException, GeneralSecurityException { - try (PEMParser parser = new PEMParser(new StringReader(secretKey))) { - Object obj = parser.readObject(); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); - - ECPrivateKey priv; - ECPublicKey pub; - - if (obj instanceof PEMKeyPair) { - KeyPair kp = converter.getKeyPair((PEMKeyPair) obj); - return kp; - } - - if (obj instanceof PrivateKeyInfo) { - // 1) get the private key - priv = (ECPrivateKey) converter.getPrivateKey((PrivateKeyInfo) obj); - - // 2) derive BC curve + base point - ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec("P-256"); - org.bouncycastle.math.ec.ECPoint Q = bcSpec.getG().multiply(priv.getS()).normalize(); - - // 3) convert BC point → JCA point - java.security.spec.ECPoint w = new java.security.spec.ECPoint( - Q.getAffineXCoord().toBigInteger(), Q.getAffineYCoord().toBigInteger()); - - // 4) build JCA curve spec from BC parameters - java.security.spec.EllipticCurve curve = new java.security.spec.EllipticCurve( - new ECFieldFp(bcSpec.getCurve().getField().getCharacteristic()), - bcSpec.getCurve().getA().toBigInteger(), bcSpec.getCurve().getB().toBigInteger(), - bcSpec.getSeed()); - java.security.spec.ECParameterSpec jcaSpec = new java.security.spec.ECParameterSpec(curve, - new java.security.spec.ECPoint(bcSpec.getG().getAffineXCoord().toBigInteger(), - bcSpec.getG().getAffineYCoord().toBigInteger()), bcSpec.getN(), - bcSpec.getH().intValue()); - - // 5) build the JCA public‐key spec and generate it - java.security.spec.ECPublicKeySpec pubSpec = new java.security.spec.ECPublicKeySpec(w, - jcaSpec); - KeyFactory kf = KeyFactory.getInstance("EC", "BC"); - pub = (ECPublicKey) kf.generatePublic(pubSpec); - - return new KeyPair(pub, priv); - } - - throw new IllegalArgumentException("Unknown PEM object: " + obj.getClass()); - } - } - - /** - * Generates a random hexadecimal string for use as a nonce in JWT headers. - * - * @param bytes Number of bytes to generate (1 byte = 2 hex characters) - * @return Random hexadecimal string of length 2 * bytes - */ - - private static String randomHex(int bytes) { - byte[] buf = new byte[bytes]; - RNG.nextBytes(buf); - StringBuilder sb = new StringBuilder(bytes * 2); - for (byte b : buf) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); - } - - /** - * Normalizes PEM-formatted key strings by: - *
    - *
  • Replacing line breaks with standard newline characters
  • - *
  • Ensuring consistent header/footer formatting
  • - *
  • Removing extraneous whitespace
  • - *
- * - * @param pem Raw PEM string (may contain escaped newlines) - * @return Normalized PEM string ready for parsing - */ - - private static String normalizePem(String pem) { - return pem.replace("\\n", "\n") - .replaceAll("-----BEGIN (.*) KEY-----\\s+", "-----BEGIN $1 KEY-----\n") - .replaceAll("\\s+\n-----END (.*) KEY-----", "-----END $1 KEY-----"); - } - - /** - *

Generates a JWT token for Coinbase Advanced Trading API authentication using the ES256 - * algorithm.

- * - *

Includes the following claims:

- *
    - *
  • {@code kid}: API key name
  • - *
  • {@code iss}: "cdp" (issuer)
  • - *
  • {@code sub}: API key name
  • - *
  • {@code nbf}: Not before timestamp (current time)
  • - *
  • {@code exp}: Expiration timestamp (2 minutes from now)
  • - *
  • {@code uri}: Concatenation of HTTP method and URI path
  • - *
  • {@code nonce}: Random 16-byte hex value
  • - *
- * - * @param method HTTP method (e.g., "GET", "POST") - * @param uri Host/path combination (e.g., "api.coinbase.com/v3/brokerage/orders") - * @return Signed JWT string - */ - - private String generateJWT(String method, String uri) { - long now = Instant.now().getEpochSecond(); - Date nbf = Date.from(Instant.ofEpochSecond(now)); - Date exp = Date.from(Instant.ofEpochSecond(now + 120)); - - Algorithm alg = Algorithm.ECDSA256(publicKey, privateKey); - - return JWT.create().withKeyId(keyName).withIssuer("cdp").withSubject(keyName).withNotBefore(nbf) - .withExpiresAt(exp).withClaim("uri", method + " " + uri) - .withHeader(Collections.singletonMap("nonce", randomHex(16))).sign(alg); - } - - /** - *

Generates the Bearer authentication header for REST API requests.

- * - *

Constructs a JWT token containing the request method and path, then formats it as a Bearer - * token:

- *
"Bearer " + JWT_TOKEN
- * - * @param restInvocation Rescu {@link RestInvocation} object containing request metadata - * @return Bearer token string for authentication header - */ - - @Override - public String digestParams(RestInvocation restInvocation) { - String hostAndPath = - restInvocation.getBaseUrl().replaceFirst("^https?://", "") + restInvocation.getPath(); - return "Bearer " + generateJWT(restInvocation.getHttpMethod(), hostAndPath); - } -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java deleted file mode 100644 index 09babfb6611..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/orders/CoinbaseOrdersResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.knowm.xchange.coinbase.v3.dto.orders; - -public class CoinbaseOrdersResponse { - -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java deleted file mode 100644 index 5cec0e19c10..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/pricebook/CoinbasePriceBooksResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.knowm.xchange.coinbase.v3.dto.pricebook; - -public class CoinbasePriceBooksResponse { - -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java deleted file mode 100644 index 119b132ffe1..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductCandlesResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.knowm.xchange.coinbase.v3.dto.products; - -public class CoinbaseProductCandlesResponse { - -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java deleted file mode 100644 index 2dda06529b8..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/products/CoinbaseProductsResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.knowm.xchange.coinbase.v3.dto.products; - -public class CoinbaseProductsResponse { - -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java deleted file mode 100644 index d64a10ea651..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountService.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.knowm.xchange.coinbase.v3.service; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.knowm.xchange.Exchange; -import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData; -import org.knowm.xchange.currency.Currency; -import org.knowm.xchange.dto.account.AccountInfo; -import org.knowm.xchange.dto.account.Balance; -import org.knowm.xchange.dto.account.Wallet; -import org.knowm.xchange.exceptions.ExchangeException; -import org.knowm.xchange.exceptions.NotAvailableFromExchangeException; -import org.knowm.xchange.exceptions.NotYetImplementedForExchangeException; -import org.knowm.xchange.service.account.AccountService; -import org.knowm.xchange.service.trade.params.DefaultWithdrawFundsParams; -import org.knowm.xchange.service.trade.params.TradeHistoryParams; -import org.knowm.xchange.service.trade.params.WithdrawFundsParams; - -public final class CoinbaseAccountService extends CoinbaseAccountServiceRaw implements - AccountService { - - public CoinbaseAccountService(Exchange exchange) { - - super(exchange); - } - - @Override - public AccountInfo getAccountInfo() throws IOException { - List wallets = new ArrayList<>(); - - List coinbaseAccounts = getCoinbaseAccounts(); - for (CoinbaseAccountData.CoinbaseAccount coinbaseAccount : coinbaseAccounts) { - CoinbaseAmount balance = coinbaseAccount.getBalance(); - Wallet wallet = Wallet.Builder.from(Arrays.asList( - new Balance(Currency.getInstance(balance.getCurrency()), balance.getAmount()))) - .id(coinbaseAccount.getId()).build(); - wallets.add(wallet); - } - - return new AccountInfo(wallets); - } - - @Override - public String withdrawFunds(WithdrawFundsParams params) - throws ExchangeException, NotAvailableFromExchangeException, NotYetImplementedForExchangeException, IOException { - if (params instanceof DefaultWithdrawFundsParams) { - DefaultWithdrawFundsParams defaultParams = (DefaultWithdrawFundsParams) params; - return withdrawFunds(defaultParams.getCurrency(), defaultParams.getAmount(), - defaultParams.getAddress()); - } - throw new IllegalStateException("Don't know how to withdraw: " + params); - } - - @Override - public TradeHistoryParams createFundingHistoryParams() { - throw new NotAvailableFromExchangeException(); - } -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java deleted file mode 100644 index a6034782532..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseAccountServiceRaw.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.knowm.xchange.coinbase.v3.service; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.knowm.xchange.Exchange; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; -import org.knowm.xchange.coinbase.v3.Coinbase; -import org.knowm.xchange.currency.Currency; - -public class CoinbaseAccountServiceRaw extends CoinbaseBaseService { - - public CoinbaseAccountServiceRaw(Exchange exchange) { - super(exchange); - } - -// public CoinbaseTransactionsResponse getTransactions(String accountId) throws IOException { -// String apiKey = exchange.getExchangeSpecification().getApiKey(); -// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() -// .getEpoch(); -// -// return coinbaseAdvancedTrade.getTransactions(Coinbase.CB_VERSION_VALUE, apiKey, -// authTokenCreator, timestamp, accountId); -// } -// -// public Map getDeposits(String accountId) throws IOException { -// String apiKey = exchange.getExchangeSpecification().getApiKey(); -// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() -// .getEpoch(); -// -// return coinbaseAdvancedTrade.getDeposits(Coinbase.CB_VERSION_VALUE, apiKey, authTokenCreator, -// timestamp, accountId); -// } -// -// public Map getWithdrawals(String accountId) throws IOException { -// String apiKey = exchange.getExchangeSpecification().getApiKey(); -// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() -// .getEpoch(); -// -// return coinbaseAdvancedTrade.getWithdrawals(Coinbase.CB_VERSION_VALUE, apiKey, -// authTokenCreator, timestamp, accountId); -// } - - /** - * Authenticated resource that shows the current user accounts. - * - * @see
developers.coinbase.com/api/v2#list-accounts - */ - public List getCoinbaseAccounts() throws IOException { - String apiKey = exchange.getExchangeSpecification().getApiKey(); - - List returnList = new ArrayList<>(); - List tmpList = null; - - String lastAccount = null; - do { - BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() - .getEpoch(); - - tmpList = coinbaseAdvancedTrade.listAccounts(authTokenCreator, 100, lastAccount).getData(); - - lastAccount = null; - if (tmpList != null && tmpList.size() > 0) { - returnList.addAll(tmpList); - lastAccount = tmpList.get(tmpList.size() - 1).getId(); - } - - } while (lastAccount != null && isValidUUID(lastAccount)); - - return returnList; - } - - private boolean isValidUUID(String uuid) { - try { - UUID.fromString(uuid); - return true; - } catch (IllegalArgumentException exception) { - return false; - } - } - - /** - * Authenticated resource that shows the current user account for the give currency. - * - * @see developers.coinbase.com/api/v2#show-an-account - */ -// public CoinbaseAccount getCoinbaseAccount(Currency currency) throws IOException { -// String apiKey = exchange.getExchangeSpecification().getApiKey(); -// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() -// .getEpoch(); -// -// return coinbaseAdvancedTrade.getAccount(Coinbase.CB_VERSION_VALUE, apiKey, authTokenCreator, -// timestamp, currency.getCurrencyCode()).getData(); -// } - - /** - * Authenticated resource that creates a new BTC account for the current user. - * - * @see developers.coinbase.com/api/v2#create-account - */ -// public CoinbaseAccount createCoinbaseAccount(String name) throws IOException { -// -// CreateCoinbaseAccountPayload payload = new CreateCoinbaseAccountPayload(name); -// -// String path = "/v2/accounts"; -// String apiKey = exchange.getExchangeSpecification().getApiKey(); -// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() -// .getEpoch(); -// String body = new ObjectMapper().writeValueAsString(payload); -// String signature = getSignature(timestamp, HttpMethod.POST, path, body); -// showCurl(HttpMethod.POST, apiKey, timestamp, signature, path, body); -// -// return coinbaseAdvancedTrade.createAccount(MediaType.APPLICATION_JSON, -// Coinbase.CB_VERSION_VALUE, apiKey, signature, timestamp, payload).getData(); -// } - - /** - * Authenticated resource that shows the current user payment methods. - * - * @see developers.coinbase.com/api/v2?shell#list-payment-methods - */ -// public List getCoinbasePaymentMethods() throws IOException { -// String apiKey = exchange.getExchangeSpecification().getApiKey(); -// BigDecimal timestamp = coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData() -// .getEpoch(); -// -// return coinbaseAdvancedTrade.getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, -// authTokenCreator, timestamp).getData(); -// } - - public static class CreateCoinbaseAccountPayload { - - @JsonProperty - String name; - - CreateCoinbaseAccountPayload(String name) { - this.name = name; - } - } -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java deleted file mode 100644 index 8f7af489b56..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/service/CoinbaseBaseService.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.knowm.xchange.coinbase.v3.service; - -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.List; -import org.knowm.xchange.Exchange; -import org.knowm.xchange.client.ExchangeRestProxyBuilder; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseTimeData.CoinbaseTime; -import org.knowm.xchange.coinbase.v3.Coinbase; -import org.knowm.xchange.coinbase.v3.CoinbaseAuthenticated; -import org.knowm.xchange.coinbase.v3.CoinbaseV3Digest; -import org.knowm.xchange.service.BaseExchangeService; -import org.knowm.xchange.service.BaseService; -import si.mazi.rescu.ParamsDigest; - -public class CoinbaseBaseService extends BaseExchangeService implements BaseService { - - protected final CoinbaseAuthenticated coinbaseAdvancedTrade; - protected final ParamsDigest authTokenCreator; - - protected CoinbaseBaseService(Exchange exchange) { - - super(exchange); - coinbaseAdvancedTrade = ExchangeRestProxyBuilder.forInterface(CoinbaseAuthenticated.class, - exchange.getExchangeSpecification()).build(); - - authTokenCreator = CoinbaseV3Digest.createInstance( - exchange.getExchangeSpecification().getApiKey(), - exchange.getExchangeSpecification().getSecretKey()); - } - - /** - * Unauthenticated resource that returns currencies supported on Coinbase. - * - * @return A list of currency names and their corresponding ISO code. - * @see developers.coinbase.com/api/v2#get-currencies - */ - public List getCoinbaseCurrencies() throws IOException { - return coinbaseAdvancedTrade.getCurrencies(Coinbase.CB_VERSION_VALUE).getData(); - } - - /** - * Unauthenticated resource that tells you the server time. - * - * @return The current server time. - * @see developers.coinbase.com/api/v2#get-current-time - */ - public CoinbaseTime getCoinbaseTime() throws IOException { - return coinbaseAdvancedTrade.getTime(Coinbase.CB_VERSION_VALUE).getData(); - } - - protected void showCurl(HttpMethod method, String apiKey, BigDecimal timestamp, String signature, - String path, String json) { - String headers = String.format( - "-H 'CB-VERSION: 2017-11-26' -H 'CB-ACCESS-KEY: %s' -H 'CB-ACCESS-SIGN: %s' -H 'CB-ACCESS-TIMESTAMP: %s'", - apiKey, signature, timestamp); - if (method == HttpMethod.GET) { - Coinbase.LOG.debug(String.format("curl %s https://api.coinbase.com%s", headers, path)); - } else if (method == HttpMethod.POST) { - String payload = "-d '" + json + "'"; - Coinbase.LOG.debug( - String.format("curl -X %s -H 'Content-Type: %s' %s %s https://api.coinbase.com%s", method, - MediaType.APPLICATION_JSON, headers, payload, path)); - } - } - - public enum HttpMethod { - GET, POST - } -} diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java deleted file mode 100644 index 6c21ab17a57..00000000000 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v3/service/AccountServiceIntegration.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.knowm.xchange.coinbase.v3.service; - -import java.util.List; -import java.util.stream.Collectors; -import org.junit.Assert; -import org.junit.Assume; -import org.junit.BeforeClass; -import org.junit.Test; -import org.knowm.xchange.Exchange; -import org.knowm.xchange.ExchangeFactory; -import org.knowm.xchange.ExchangeSpecification; -import org.knowm.xchange.coinbase.v3.CoinbaseExchange; -import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; -import org.knowm.xchange.currency.Currency; -import org.knowm.xchange.service.account.AccountService; -import org.knowm.xchange.utils.AuthUtils; - -public class AccountServiceIntegration { - - static Exchange exchange; - static AccountService accountService; - - @BeforeClass - public static void beforeClass() { - ExchangeSpecification exchangeSpecification = ExchangeFactory.INSTANCE.createExchange(CoinbaseExchange.class).getDefaultExchangeSpecification(); - AuthUtils.setApiAndSecretKey(exchangeSpecification); - exchange = ExchangeFactory.INSTANCE.createExchange(exchangeSpecification); - accountService = exchange.getAccountService(); - } - - @Test - public void listAccounts() throws Exception { - - Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); - - CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; - List accounts = coinbaseService.getCoinbaseAccounts(); - Assert.assertTrue(accounts.size() > 0); - - CoinbaseAccount btcAccount = - accounts.stream() - .filter(t -> t.getName().equals("BTC Wallet")) - .collect(Collectors.toList()) - .get(0); - Assert.assertEquals("BTC", btcAccount.getBalance().getCurrency()); - Assert.assertEquals("BTC Wallet", btcAccount.getName()); - } - - @Test - public void getAccountByCurrency() throws Exception { - - Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); - - CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; -// CoinbaseAccount btcAccount = coinbaseService.getCoinbaseAccount(Currency.BTC); -// Assert.assertEquals("BTC", btcAccount.getBalance().getCurrency()); -// Assert.assertEquals("BTC Wallet", btcAccount.getName()); - } - - @Test - public void createAccount() throws Exception { - - Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); - - CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; - try { - //coinbaseService.createCoinbaseAccount("BTC Test"); - } catch (CoinbaseException ex) { - Assert.assertEquals(400, ex.getHttpStatusCode()); - Assert.assertEquals( - "Creation of multiple BTC accounts is not supported (HTTP status code: 400)", - ex.getMessage()); - } - } - - @Test - public void listPaymentMethods() throws Exception { - - Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); - - CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; -// List methods = coinbaseService.getCoinbasePaymentMethods(); -// Assert.assertTrue(methods.size() > 0); - } -} From a3c6d77b5ba70ea2aa85a21f7756914d1ac65197 Mon Sep 17 00:00:00 2001 From: David Pang Date: Mon, 19 May 2025 15:47:21 -0400 Subject: [PATCH 09/10] updated v2 market data service --- .../knowm/xchange/coinbase/v2/Coinbase.java | 10 ++- .../coinbase/v2/CoinbaseAuthenticated.java | 16 ---- .../CoinbaseCryptocurrencyData.java | 64 +++++++++++++++ .../dto/marketdata/CoinbaseCurrencyData.java | 77 ------------------- .../marketdata/CoinbaseFiatCurrencyData.java | 72 +++++++++++++++++ .../v2/service/CoinbaseAccountServiceRaw.java | 50 ------------ .../v2/service/CoinbaseBaseService.java | 60 ++++++++------- .../CoinbaseMarketDataJsonTest.java | 12 +-- .../v2/service/AccountServiceIntegration.java | 26 ------- .../service/MarketDataServiceIntegration.java | 40 ++++++---- .../v2/marketdata/CoinbaseMarketDataDemo.java | 4 +- 11 files changed, 206 insertions(+), 225 deletions(-) create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCryptocurrencyData.java delete mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCurrencyData.java create mode 100644 xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseFiatCurrencyData.java diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/Coinbase.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/Coinbase.java index bc8f6e36564..72ac49a5c09 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/Coinbase.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/Coinbase.java @@ -9,8 +9,9 @@ import jakarta.ws.rs.core.MediaType; import java.io.IOException; import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCryptocurrencyData; import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseExchangeRateData; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseFiatCurrencyData; import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbasePriceData; import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseTimeData; import org.slf4j.Logger; @@ -33,7 +34,12 @@ public interface Coinbase { @GET @Path("currencies") - CoinbaseCurrencyData getCurrencies(@HeaderParam(CB_VERSION) String apiVersion) + CoinbaseFiatCurrencyData getFiatCurrencies(@HeaderParam(CB_VERSION) String apiVersion) + throws IOException, CoinbaseException; + + @GET + @Path("currencies/crypto") + CoinbaseCryptocurrencyData getCryptocurrencies(@HeaderParam(CB_VERSION) String apiVersion) throws IOException, CoinbaseException; @GET diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java index 4fe097e667c..3fdcbdd33fe 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/CoinbaseAuthenticated.java @@ -105,22 +105,6 @@ CoinbaseAccountsData getAccounts( CoinbaseAccountData getAccount(@HeaderParam(CB_AUTHORIZATION_KEY) ParamsDigest authTokenGenerator, @PathParam("currency") String currency) throws IOException, CoinbaseException; - @POST - @Path("accounts") - @Consumes(MediaType.APPLICATION_JSON) - CoinbaseAccountData createAccount(@HeaderParam(CONTENT_TYPE) String contentType, - @HeaderParam(CB_VERSION) String apiVersion, @HeaderParam(CB_ACCESS_KEY) String apiKey, - @HeaderParam(CB_ACCESS_SIGN) String signature, - @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, Object payload) - throws IOException, CoinbaseException; - - @GET - @Path("payment-methods") - CoinbasePaymentMethodsData getPaymentMethods(@HeaderParam(CB_VERSION) String apiVersion, - @HeaderParam(CB_ACCESS_KEY) String apiKey, - @HeaderParam(CB_ACCESS_SIGN) CoinbaseV2Digest signature, - @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp) throws IOException, CoinbaseException; - @POST @Path("accounts/{account}/buys") @Consumes(MediaType.APPLICATION_JSON) diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCryptocurrencyData.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCryptocurrencyData.java new file mode 100644 index 00000000000..c8a5ca662d0 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCryptocurrencyData.java @@ -0,0 +1,64 @@ +package org.knowm.xchange.coinbase.v2.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.List; + +public class CoinbaseCryptocurrencyData { + + private List data; + + public List getData() { + return Collections.unmodifiableList(data); + } + + public void setData(List data) { + this.data = data; + } + + public static class CoinbaseCryptocurrency { + + private final String name; + private final String code; + + @JsonCreator + public CoinbaseCryptocurrency(@JsonProperty("name") String name, @JsonProperty("code") String code) { + this.name = name; + this.code = code; + } + + public String getName() { + return name; + } + + public String getCode() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CoinbaseCryptocurrency other = (CoinbaseCryptocurrency) obj; + return code.equals(other.code); + } + + @Override + public String toString() { + return code + " (" + name + ")"; + } + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCurrencyData.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCurrencyData.java deleted file mode 100644 index 25f454dffa7..00000000000 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseCurrencyData.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.knowm.xchange.coinbase.v2.dto.marketdata; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public class CoinbaseCurrencyData { - - private List data; - - public List getData() { - return Collections.unmodifiableList(data); - } - - public void setData(List data) { - this.data = data; - } - - @JsonDeserialize(using = CoinbaseCurrencyDeserializer.class) - public static class CoinbaseCurrency { - private final String name; - private final String id; - - public CoinbaseCurrency(String name, final String id) { - this.name = name; - this.id = id; - } - - public String getName() { - return name; - } - - public String getId() { - return id; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - CoinbaseCurrency other = (CoinbaseCurrency) obj; - return id.equals(other.id); - } - - @Override - public String toString() { - return id + " (" + name + ")"; - } - } - - // [TODO] can we not do this with @JsonCreator - static class CoinbaseCurrencyDeserializer extends JsonDeserializer { - - @Override - public CoinbaseCurrency deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException { - ObjectCodec oc = jp.getCodec(); - JsonNode node = oc.readTree(jp); - String name = node.get("name").asText(); - String id = node.get("id").asText(); - return new CoinbaseCurrency(name, id); - } - } -} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseFiatCurrencyData.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseFiatCurrencyData.java new file mode 100644 index 00000000000..3b3a0c0dba9 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseFiatCurrencyData.java @@ -0,0 +1,72 @@ +package org.knowm.xchange.coinbase.v2.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +public class CoinbaseFiatCurrencyData { + + private List data; + + public List getData() { + return Collections.unmodifiableList(data); + } + + public void setData(List data) { + this.data = data; + } + + public static class CoinbaseFiatCurrency { + + private final String name; + private final String id; + private final BigDecimal minSize; + + @JsonCreator + public CoinbaseFiatCurrency(@JsonProperty("name") String name, @JsonProperty("id") String id, + @JsonProperty("min_size") BigDecimal minSize) { + this.name = name; + this.id = id; + this.minSize = minSize; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + public BigDecimal getMinSize() { + return minSize; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CoinbaseFiatCurrency other = (CoinbaseFiatCurrency) obj; + return id.equals(other.id); + } + + @Override + public String toString() { + return id + " (" + name + ")"; + } + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java index 073e93929c6..3a63e26ae65 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountServiceRaw.java @@ -1,8 +1,5 @@ package org.knowm.xchange.coinbase.v2.service; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; @@ -12,7 +9,6 @@ import org.knowm.xchange.Exchange; import org.knowm.xchange.coinbase.v2.Coinbase; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; -import org.knowm.xchange.coinbase.v2.dto.account.CoinbasePaymentMethodsData.CoinbasePaymentMethod; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseTransactionsResponse; import org.knowm.xchange.currency.Currency; @@ -58,8 +54,6 @@ public List getCoinbaseAccounts() throws IOException { String lastAccount = null; do { - BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - tmpList = coinbase.getAccounts(authTokenGenerator, 100, lastAccount).getData(); lastAccount = null; @@ -92,48 +86,4 @@ public CoinbaseAccount getCoinbaseAccount(Currency currency) throws IOException return coinbase.getAccount(authTokenGenerator, currency.getCurrencyCode()).getData(); } - /** - * Authenticated resource that creates a new BTC account for the current user. - * - * @see developers.coinbase.com/api/v2#create-account - */ - public CoinbaseAccount createCoinbaseAccount(String name) throws IOException { - - CreateCoinbaseAccountPayload payload = new CreateCoinbaseAccountPayload(name); - - String path = "/v2/accounts"; - String apiKey = exchange.getExchangeSpecification().getApiKey(); - BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - String body = new ObjectMapper().writeValueAsString(payload); - String signature = getSignature(timestamp, HttpMethod.POST, path, body); - showCurl(HttpMethod.POST, apiKey, timestamp, signature, path, body); - - return coinbase.createAccount(MediaType.APPLICATION_JSON, Coinbase.CB_VERSION_VALUE, apiKey, - signature, timestamp, payload).getData(); - } - - /** - * Authenticated resource that shows the current user payment methods. - * - * @see developers.coinbase.com/api/v2?shell#list-payment-methods - */ - public List getCoinbasePaymentMethods() throws IOException { - String apiKey = exchange.getExchangeSpecification().getApiKey(); - BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); - - return coinbase.getPaymentMethods(Coinbase.CB_VERSION_VALUE, apiKey, authTokenGenerator, - timestamp).getData(); - } - - public static class CreateCoinbaseAccountPayload { - - @JsonProperty - String name; - - CreateCoinbaseAccountPayload(String name) { - this.name = name; - } - } } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java index 7379923be2b..20ffbb8c26e 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseBaseService.java @@ -12,7 +12,8 @@ import org.knowm.xchange.coinbase.v2.Coinbase; import org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated; import org.knowm.xchange.coinbase.v2.CoinbaseV2Digest; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCryptocurrencyData.CoinbaseCryptocurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseFiatCurrencyData.CoinbaseFiatCurrency; import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseTimeData.CoinbaseTime; import org.knowm.xchange.service.BaseExchangeService; import org.knowm.xchange.service.BaseService; @@ -26,25 +27,34 @@ public class CoinbaseBaseService extends BaseExchangeService implements BaseServ protected CoinbaseBaseService(Exchange exchange) { super(exchange); - coinbase = - ExchangeRestProxyBuilder.forInterface( - CoinbaseAuthenticated.class, exchange.getExchangeSpecification()) - .build(); + coinbase = ExchangeRestProxyBuilder.forInterface(CoinbaseAuthenticated.class, + exchange.getExchangeSpecification()).build(); - authTokenGenerator = - CoinbaseV2Digest.createInstance(exchange.getExchangeSpecification().getApiKey(), exchange.getExchangeSpecification().getSecretKey()); + authTokenGenerator = CoinbaseV2Digest.createInstance( + exchange.getExchangeSpecification().getApiKey(), + exchange.getExchangeSpecification().getSecretKey()); } /** - * Unauthenticated resource that returns currencies supported on Coinbase. + * Unauthenticated resource that returns cryptocurrencies supported on Coinbase. * - * @return A list of currency names and their corresponding ISO code. + * @return A list of cryptocurrency names and their corresponding ISO code. * @see developers.coinbase.com/api/v2#get-currencies + * href="https://docs.cdp.coinbase.com/coinbase-app/docs/track/api-currencies">https://docs.cdp.coinbase.com/coinbase-app/docs/track/api-currencies */ - public List getCoinbaseCurrencies() throws IOException { + public List getCoinbaseCryptocurrencies() throws IOException { + return coinbase.getCryptocurrencies(Coinbase.CB_VERSION_VALUE).getData(); + } - return coinbase.getCurrencies(Coinbase.CB_VERSION_VALUE).getData(); + /** + * Unauthenticated resource that returns fiat currencies supported on Coinbase. + * + * @return A list of fiat currency names and their corresponding ISO code. + * @see https://docs.cdp.coinbase.com/coinbase-app/docs/track/api-currencies + */ + public List getCoinbaseFiatCurrencies() throws IOException { + return coinbase.getFiatCurrencies(Coinbase.CB_VERSION_VALUE).getData(); } /** @@ -52,7 +62,7 @@ public List getCoinbaseCurrencies() throws IOException { * * @return The current server time. * @see developers.coinbase.com/api/v2#get-current-time + * href="https://developers.coinbase.com/api/v2#get-current-time">developers.coinbase.com/api/v2#get-current-time */ public CoinbaseTime getCoinbaseTime() throws IOException { @@ -67,30 +77,22 @@ protected String getSignature(BigDecimal timestamp, HttpMethod method, String pa return DigestUtils.bytesToHex(bytes); } - protected void showCurl( - HttpMethod method, - String apiKey, - BigDecimal timestamp, - String signature, - String path, - String json) { - String headers = - String.format( - "-H 'CB-VERSION: 2017-11-26' -H 'CB-ACCESS-KEY: %s' -H 'CB-ACCESS-SIGN: %s' -H 'CB-ACCESS-TIMESTAMP: %s'", - apiKey, signature, timestamp); + protected void showCurl(HttpMethod method, String apiKey, BigDecimal timestamp, String signature, + String path, String json) { + String headers = String.format( + "-H 'CB-VERSION: 2017-11-26' -H 'CB-ACCESS-KEY: %s' -H 'CB-ACCESS-SIGN: %s' -H 'CB-ACCESS-TIMESTAMP: %s'", + apiKey, signature, timestamp); if (method == HttpMethod.GET) { Coinbase.LOG.debug(String.format("curl %s https://api.coinbase.com%s", headers, path)); } else if (method == HttpMethod.POST) { String payload = "-d '" + json + "'"; Coinbase.LOG.debug( - String.format( - "curl -X %s -H 'Content-Type: %s' %s %s https://api.coinbase.com%s", - method, MediaType.APPLICATION_JSON, headers, payload, path)); + String.format("curl -X %s -H 'Content-Type: %s' %s %s https://api.coinbase.com%s", method, + MediaType.APPLICATION_JSON, headers, payload, path)); } } public enum HttpMethod { - GET, - POST + GET, POST } } diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseMarketDataJsonTest.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseMarketDataJsonTest.java index ce9dae5ec52..8565ed06e43 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseMarketDataJsonTest.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/dto/marketdata/CoinbaseMarketDataJsonTest.java @@ -11,7 +11,7 @@ import java.util.Map; import org.junit.Test; import org.knowm.xchange.coinbase.v2.dto.CoinbasePrice; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCryptocurrencyData.CoinbaseCryptocurrency; public class CoinbaseMarketDataJsonTest { @@ -45,14 +45,14 @@ public void testDeserializeCurrencies() throws IOException { // Use Jackson to parse it ObjectMapper mapper = new ObjectMapper(); - JavaType javaType = mapper.getTypeFactory().constructType(CoinbaseCurrencyData.class); - CoinbaseCurrencyData rawdata = mapper.readValue(is, javaType); + JavaType javaType = mapper.getTypeFactory().constructType(CoinbaseCryptocurrencyData.class); + CoinbaseCryptocurrencyData rawdata = mapper.readValue(is, javaType); - List currencies = rawdata.getData(); + List currencies = rawdata.getData(); assertThat(currencies.size()).isEqualTo(168); - CoinbaseCurrency currency = currencies.get(167); - assertThat(currency.getId()).isEqualTo("ZWL"); + CoinbaseCryptocurrency currency = currencies.get(167); + assertThat(currency.getCode()).isEqualTo("ZWL"); assertThat(currency.getName()).isEqualTo("Zimbabwean Dollar"); } diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java index 5592b7b7c44..c9b47298229 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java @@ -58,30 +58,4 @@ public void getAccountByCurrency() throws Exception { Assert.assertEquals("BTC", btcAccount.getBalance().getCurrency()); Assert.assertEquals("BTC Wallet", btcAccount.getName()); } - - @Test - public void createAccount() throws Exception { - - Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); - - CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; - try { - coinbaseService.createCoinbaseAccount("BTC Test"); - } catch (CoinbaseException ex) { - Assert.assertEquals(400, ex.getHttpStatusCode()); - Assert.assertEquals( - "Creation of multiple BTC accounts is not supported (HTTP status code: 400)", - ex.getMessage()); - } - } - - @Test - public void listPaymentMethods() throws Exception { - - Assume.assumeNotNull(exchange.getExchangeSpecification().getApiKey()); - - CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; - List methods = coinbaseService.getCoinbasePaymentMethods(); - Assert.assertTrue(methods.size() > 0); - } } diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java index 24a8a70e9c2..6b59fc59440 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/MarketDataServiceIntegration.java @@ -1,19 +1,20 @@ package org.knowm.xchange.coinbase.v2.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; import java.math.BigDecimal; import java.util.Date; import java.util.List; import java.util.Map; -import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.knowm.xchange.Exchange; import org.knowm.xchange.ExchangeFactory; import org.knowm.xchange.coinbase.v2.CoinbaseExchange; import org.knowm.xchange.coinbase.v2.dto.CoinbasePrice; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCryptocurrencyData.CoinbaseCryptocurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseFiatCurrencyData.CoinbaseFiatCurrency; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.service.marketdata.MarketDataService; @@ -32,19 +33,29 @@ public static void beforeClass() { } @Test - public void listCurrencies() throws Exception { + public void listCryptocurrencies() throws Exception { + CoinbaseMarketDataService coinbaseService = (CoinbaseMarketDataService) marketDataService; + List currencies = coinbaseService.getCoinbaseCryptocurrencies(); + + assertTrue(currencies.stream() + .anyMatch(crypto -> crypto.getName().equals("Bitcoin") && crypto.getCode().equals("BTC"))); + } + @Test + public void listFiatCurrencies() throws Exception { CoinbaseMarketDataService coinbaseService = (CoinbaseMarketDataService) marketDataService; - List currencies = coinbaseService.getCoinbaseCurrencies(); - assertThat(currencies).contains(new CoinbaseCurrency("Bitcoin", "BTC")); + List currencies = coinbaseService.getCoinbaseFiatCurrencies(); + + assertTrue(currencies.stream() + .anyMatch(fiat -> fiat.getName().equals("Euro") && fiat.getId().equals("EUR"))); } @Test - public void listExchageRates() throws Exception { + public void listExchangeRates() throws Exception { CoinbaseMarketDataService coinbaseService = (CoinbaseMarketDataService) marketDataService; Map exchangeRates = coinbaseService.getCoinbaseExchangeRates(); - Assert.assertTrue(exchangeRates.get("EUR") instanceof BigDecimal); + assertTrue(exchangeRates.get("EUR") instanceof BigDecimal); } @Test @@ -52,28 +63,23 @@ public void listPrices() throws Exception { CoinbaseMarketDataService coinbaseService = (CoinbaseMarketDataService) marketDataService; CoinbasePrice money = coinbaseService.getCoinbaseBuyPrice(Currency.BTC, Currency.USD); - assertThat(money) - .hasFieldOrPropertyWithValue("currency", Currency.USD) + assertThat(money).hasFieldOrPropertyWithValue("currency", Currency.USD) .hasNoNullFieldsOrProperties(); money = coinbaseService.getCoinbaseSellPrice(Currency.BTC, Currency.USD); - assertThat(money) - .hasFieldOrPropertyWithValue("currency", Currency.USD) + assertThat(money).hasFieldOrPropertyWithValue("currency", Currency.USD) .hasNoNullFieldsOrProperties(); money = coinbaseService.getCoinbaseSpotRate(Currency.BTC, Currency.USD); - assertThat(money) - .hasFieldOrPropertyWithValue("currency", Currency.USD) + assertThat(money).hasFieldOrPropertyWithValue("currency", Currency.USD) .hasNoNullFieldsOrProperties(); money = coinbaseService.getCoinbaseSpotRate(Currency.BTC, Currency.USD); - assertThat(money) - .hasFieldOrPropertyWithValue("currency", Currency.USD) + assertThat(money).hasFieldOrPropertyWithValue("currency", Currency.USD) .hasNoNullFieldsOrProperties(); money = coinbaseService.getCoinbaseHistoricalSpotRate(Currency.BTC, Currency.USD, new Date()); - assertThat(money) - .hasFieldOrPropertyWithValue("currency", Currency.USD) + assertThat(money).hasFieldOrPropertyWithValue("currency", Currency.USD) .hasNoNullFieldsOrProperties(); } } diff --git a/xchange-examples/src/main/java/org/knowm/xchange/examples/coinbase/v2/marketdata/CoinbaseMarketDataDemo.java b/xchange-examples/src/main/java/org/knowm/xchange/examples/coinbase/v2/marketdata/CoinbaseMarketDataDemo.java index 9fad740cebe..b2ca94821fb 100644 --- a/xchange-examples/src/main/java/org/knowm/xchange/examples/coinbase/v2/marketdata/CoinbaseMarketDataDemo.java +++ b/xchange-examples/src/main/java/org/knowm/xchange/examples/coinbase/v2/marketdata/CoinbaseMarketDataDemo.java @@ -10,7 +10,7 @@ import org.knowm.xchange.ExchangeFactory; import org.knowm.xchange.coinbase.v2.CoinbaseExchange; import org.knowm.xchange.coinbase.v2.dto.CoinbasePrice; -import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCryptocurrencyData.CoinbaseCryptocurrency; import org.knowm.xchange.coinbase.v2.service.CoinbaseMarketDataService; import org.knowm.xchange.currency.Currency; @@ -22,7 +22,7 @@ public static void main(String[] args) throws IOException, ParseException { CoinbaseMarketDataService marketDataService = (CoinbaseMarketDataService) coinbaseExchange.getMarketDataService(); - List currencies = marketDataService.getCoinbaseCurrencies(); + List currencies = marketDataService.getCoinbaseCryptocurrencies(); System.out.println("Currencies: " + currencies); Map exchangeRates = marketDataService.getCoinbaseExchangeRates(); From 871b296f812d87f98745da9a630c3429e02af09f Mon Sep 17 00:00:00 2001 From: David Pang Date: Mon, 19 May 2025 17:20:37 -0400 Subject: [PATCH 10/10] WIP trade servie --- .../v2/service/AccountServiceIntegration.java | 2 +- .../v2/service/TradeServiceIntegration.java | 37 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java index c9b47298229..fabd394f2c6 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/AccountServiceIntegration.java @@ -37,7 +37,7 @@ public void listAccounts() throws Exception { CoinbaseAccountService coinbaseService = (CoinbaseAccountService) accountService; List accounts = coinbaseService.getCoinbaseAccounts(); - Assert.assertTrue(accounts.size() > 0); + Assert.assertFalse(accounts.isEmpty()); CoinbaseAccount btcAccount = accounts.stream() diff --git a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java index 5aed4c308d1..71225c192ac 100644 --- a/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java +++ b/xchange-coinbase/src/test/java/org/knowm/xchange/coinbase/v2/service/TradeServiceIntegration.java @@ -2,31 +2,39 @@ import java.io.IOException; import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.knowm.xchange.Exchange; import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.ExchangeSpecification; import org.knowm.xchange.coinbase.v2.CoinbaseExchange; import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; import org.knowm.xchange.coinbase.v2.dto.CoinbasePrice; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseBuyData.CoinbaseBuy; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseSellData.CoinbaseSell; import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.service.account.AccountService; import org.knowm.xchange.service.trade.TradeService; import org.knowm.xchange.utils.AuthUtils; public class TradeServiceIntegration { static Exchange exchange; - static TradeService tradeService; + static CoinbaseTradeService tradeService; + static CoinbaseAccountService accountService; @BeforeClass public static void beforeClass() { - exchange = ExchangeFactory.INSTANCE.createExchange(CoinbaseExchange.class); - AuthUtils.setApiAndSecretKey(exchange.getExchangeSpecification()); - tradeService = exchange.getTradeService(); + ExchangeSpecification exchangeSpecification = ExchangeFactory.INSTANCE.createExchange(CoinbaseExchange.class).getDefaultExchangeSpecification(); + AuthUtils.setApiAndSecretKey(exchangeSpecification); + exchange = ExchangeFactory.INSTANCE.createExchange(exchangeSpecification); + tradeService = (CoinbaseTradeService) exchange.getTradeService(); + accountService = (CoinbaseAccountService) exchange.getAccountService(); } @Test @@ -38,15 +46,14 @@ public void buy() throws Exception { BigDecimal amount = new BigDecimal("10.00"); BigDecimal total = new BigDecimal("10.00"); - CoinbaseTradeService coinbaseService = (CoinbaseTradeService) tradeService; - CoinbaseBuy res = coinbaseService.buy(accountId(currency), total, currency, false); + CoinbaseBuy res = tradeService.buy(getAccountId(currency), total, currency, false); Assert.assertNotNull(res.getId()); Assert.assertEquals("created", res.getStatus()); Assert.assertEquals(new CoinbasePrice(new BigDecimal("1.00"), Currency.EUR), res.getFee()); Assert.assertEquals(new CoinbaseAmount("BTC", new BigDecimal("0.0001")), res.getAmount()); Assert.assertEquals(Currency.EUR, res.getSubtotal().getCurrency()); Assert.assertEquals(Currency.EUR, res.getTotal().getCurrency()); - Assert.assertEquals(false, res.isCommitted()); + Assert.assertFalse(res.isCommitted()); } @Test @@ -58,15 +65,14 @@ public void sell() throws Exception { BigDecimal amount = new BigDecimal("0.0001"); BigDecimal total = null; - CoinbaseTradeService coinbaseService = (CoinbaseTradeService) tradeService; - CoinbaseSell res = coinbaseService.sell(accountId(currency), total, currency, false); + CoinbaseSell res = tradeService.sell(getAccountId(currency), total, currency, false); Assert.assertNotNull(res.getId()); Assert.assertEquals("created", res.getStatus()); Assert.assertEquals(new CoinbasePrice(new BigDecimal("1.00"), Currency.EUR), res.getFee()); Assert.assertEquals(new CoinbaseAmount("BTC", new BigDecimal("0.0001")), res.getAmount()); Assert.assertEquals(Currency.EUR, res.getSubtotal().getCurrency()); Assert.assertEquals(Currency.EUR, res.getTotal().getCurrency()); - Assert.assertEquals(false, res.isCommitted()); + Assert.assertFalse(res.isCommitted()); } @Test @@ -78,19 +84,18 @@ public void quote() throws Exception { BigDecimal amount = new BigDecimal("0.0001"); BigDecimal total = null; - CoinbaseTradeService coinbaseService = (CoinbaseTradeService) tradeService; - CoinbaseSell res = coinbaseService.quote(accountId(currency), total, currency); + CoinbaseSell res = tradeService.quote(getAccountId(currency), amount, currency); Assert.assertNull(res.getId()); Assert.assertEquals("quote", res.getStatus()); Assert.assertEquals(new CoinbasePrice(new BigDecimal("1.00"), Currency.EUR), res.getFee()); Assert.assertEquals(new CoinbaseAmount("BTC", new BigDecimal("0.0001")), res.getAmount()); Assert.assertEquals(Currency.EUR, res.getSubtotal().getCurrency()); Assert.assertEquals(Currency.EUR, res.getTotal().getCurrency()); - Assert.assertEquals(false, res.isCommitted()); + Assert.assertFalse(res.isCommitted()); } - private String accountId(Currency currency) throws IOException { - CoinbaseAccountService accountService = (CoinbaseAccountService) exchange.getAccountService(); - return accountService.getCoinbaseAccount(currency).getId(); + private String getAccountId(Currency currency) throws IOException { + CoinbaseAccount account = accountService.getCoinbaseAccount(currency); + return account.getId(); } }