diff --git a/xchange-coinbase/pom.xml b/xchange-coinbase/pom.xml index 2b66f1a1b23..49292502842 100644 --- a/xchange-coinbase/pom.xml +++ b/xchange-coinbase/pom.xml @@ -30,7 +30,31 @@ xchange-core ${project.version} + + org.projectlombok + lombok + + + + + com.nimbusds + nimbus-jose-jwt + 9.40 + + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/CoinbaseAdapters.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/CoinbaseAdapters.java index d06f09716cf..806928fa2ea 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/CoinbaseAdapters.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/CoinbaseAdapters.java @@ -20,6 +20,7 @@ import org.knowm.xchange.dto.Order.OrderType; import org.knowm.xchange.dto.account.AccountInfo; import org.knowm.xchange.dto.account.Balance; +import org.knowm.xchange.dto.account.FundingRecord; import org.knowm.xchange.dto.account.Wallet; import org.knowm.xchange.dto.marketdata.Ticker; import org.knowm.xchange.dto.marketdata.Trades.TradeSortType; @@ -56,8 +57,76 @@ public static UserTrades adaptTrades(List transactions, OrderTy return new UserTrades(trades, TradeSortType.SortByTimestamp); } + public static List adaptFundings(List trades) { + final List records = new ArrayList<>(); + + for (CoinbaseBuySell record : trades) { + records.add(adaptFunding(record)); + } + + return records; + } + + private static FundingRecord adaptFunding(CoinbaseBuySell transaction) { + + FundingRecord.Type type = null; + FundingRecord.Status status; + String recordType = transaction.getResource().toUpperCase(); + + switch (recordType) { + case "WITHDRAWAL": + case "CREATE_VOUCHER": + type = FundingRecord.Type.WITHDRAWAL; + break; + case "DEPOSIT": + case "USED_VOUCHER": + case "NEW_USER_REWARD": + case "REFERRAL": + type = FundingRecord.Type.DEPOSIT; + break; + default: + // here we ignore the other types which are trading + } + + switch (transaction.getStatus().toUpperCase()) { + case "OK": + case "COMPLETED": + status = FundingRecord.Status.COMPLETE; + break; + case "NEW": + case "SENT": + case "CREATED": + case "WAITING": + case "PENDING": + status = FundingRecord.Status.PROCESSING; + break; + default: + status = FundingRecord.Status.FAILED; + } + + FundingRecord funding = + new FundingRecord( + null, + Date.from(transaction.getCreatedAt().toInstant()), + Currency.getInstance(transaction.getAmount().getCurrency()), + transaction.getAmount().getAmount(), + transaction.getId(), + null, + type, + status, + null, + transaction.getFee().getAmount(), + null); + return funding; + } + private static UserTrade adaptTrade(CoinbaseBuySell transaction, OrderType orderType) { - return UserTrade.builder() + // Bug fix - Null point exception in case of cancelled transactions + + String transactionId = transaction.getTransaction() == null ? + null : (transaction.getTransaction().getId() == null ? + null : transaction.getTransaction().getId()); + return new UserTrade.Builder() .type(orderType) .originalAmount(transaction.getAmount().getAmount()) .currencyPair( @@ -70,7 +139,7 @@ private static UserTrade adaptTrade(CoinbaseBuySell transaction, OrderType order .divide(transaction.getAmount().getAmount(), PRICE_SCALE, RoundingMode.HALF_UP)) .timestamp(Date.from(transaction.getCreatedAt().toInstant())) .id(transaction.getId()) - .orderId(transaction.getTransaction().getId()) + .orderId(transactionId) .feeAmount(transaction.getFee().getAmount()) .feeCurrency(Currency.getInstance(transaction.getFee().getCurrency())) .build(); diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseAuthenticatedCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseAuthenticatedCDP.java new file mode 100644 index 00000000000..229810d292e --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseAuthenticatedCDP.java @@ -0,0 +1,189 @@ +package org.knowm.xchange.coinbase.cdp; + +import org.knowm.xchange.coinbase.v2.Coinbase; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData; +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.CoinbaseExpandTransactionsResponse; +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 si.mazi.rescu.ParamsDigest; + + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Map; + +@Path("/v2") +@Produces(MediaType.APPLICATION_JSON) +public interface CoinbaseAuthenticatedCDP extends Coinbase { + + /** + * All API key requests must be signed and contain the following headers. + * + *

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 + * 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 + * GET requests). + * + *

The method should be UPPER CASE. + * + *

developers.coinbase.com/api/v2#api-key + */ + String CB_ACCESS_KEY = "CB-ACCESS-KEY"; + + + String CB_ACCESS_SIGN = "CB-ACCESS-SIGN"; + String CB_ACCESS_TIMESTAMP = "CB-ACCESS-TIMESTAMP"; + + String CONTENT_TYPE = "Content-Type"; + + @GET + @Path("accounts/{accountId}/transactions") + CoinbaseTransactionsResponse getTransactions( + @HeaderParam("Authorization") ParamsDigest signature, + @PathParam("accountId") String accountId) + throws IOException, CoinbaseException; + + @GET + @Path("accounts/{accountId}/transactions") + CoinbaseExpandTransactionsResponse getExpandedTransactions( + @HeaderParam("Authorization") ParamsDigest signature, + @PathParam("accountId") String accountId, + @QueryParam("limit") int limit, + @QueryParam("order") String orderType, + @QueryParam("starting_after") String startingFrom) + throws IOException, CoinbaseException; + + @GET + @Path("accounts/{accountId}/buys") + CoinbaseBuySellResponse getBuys( + @HeaderParam("Authorization") ParamsDigest signature, + @PathParam("accountId") String accountId, + @QueryParam("limit") Integer limit, + @QueryParam("starting_after") String startingAfter) + throws IOException, CoinbaseException; + + @GET + @Path("accounts/{accountId}/sells") + CoinbaseBuySellResponse getSells( + @HeaderParam("Authorization") ParamsDigest signature, + @PathParam("accountId") String accountId, + @QueryParam("limit") Integer limit, + @QueryParam("starting_after") String startingAfter) + throws IOException, CoinbaseException; + + @GET + @Path("accounts/{accountId}/deposits") + CoinbaseBuySellResponse getAllDeposits( + @HeaderParam("Authorization") ParamsDigest signature, + @PathParam("accountId") String accountId, + @QueryParam("limit") Integer limit, + @QueryParam("starting_after") String startingAfter) + throws IOException, CoinbaseException; + + @GET + @Path("accounts/{accountId}/withdrawals") + CoinbaseBuySellResponse getAllWithdrawals( + @HeaderParam("Authorization") ParamsDigest signature, + @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, + @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; + + @GET + @Path("accounts/{accountId}/withdrawals") + 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; + + @GET + @Path("accounts") + CoinbaseAccountsData getAccounts( + @HeaderParam("Authorization") ParamsDigest signature, + @QueryParam("limit") Integer limit, + @QueryParam("starting_after") String starting_after) + throws IOException, CoinbaseException; + + @GET + @Path("accounts/{currency}") + CoinbaseAccountData getAccount( + @HeaderParam("Authorization") ParamsDigest signature, + @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("Authorization") ParamsDigest signature) + 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, + @HeaderParam(CB_ACCESS_SIGN) String signature, + @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, + @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, + @HeaderParam(CB_ACCESS_SIGN) String signature, + @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, + @PathParam("account") String accountId, + Object payload) + throws IOException, CoinbaseException; +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseExchangeCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseExchangeCDP.java new file mode 100644 index 00000000000..cedb4c2ce9e --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseExchangeCDP.java @@ -0,0 +1,30 @@ +package org.knowm.xchange.coinbase.cdp; + +import org.knowm.xchange.BaseExchange; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.coinbase.v2.service.CoinbaseMarketDataService; +import org.knowm.xchange.coinbase.cdp.service.CoinbaseAccountServiceCDP; +import org.knowm.xchange.coinbase.cdp.service.CoinbaseTradeServiceCDP; + +public class CoinbaseExchangeCDP extends BaseExchange implements Exchange { + + @Override + protected void initServices() { + this.marketDataService = new CoinbaseMarketDataService(this); + this.accountService = new CoinbaseAccountServiceCDP(this); + this.tradeService = new CoinbaseTradeServiceCDP(this); + } + + @Override + public ExchangeSpecification getDefaultExchangeSpecification() { + + final ExchangeSpecification exchangeSpecification = new ExchangeSpecification(this.getClass()); + exchangeSpecification.setSslUri("https://api.coinbase.com"); + exchangeSpecification.setHost("api.coinbase.com"); + exchangeSpecification.setExchangeName("Coinbase"); + exchangeSpecification.setExchangeDescription( + "Founded in June of 2012, Coinbase is a bitcoin wallet and platform where merchants and consumers can transact with the new digital currency bitcoin."); + return exchangeSpecification; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseV2DigestCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseV2DigestCDP.java new file mode 100644 index 00000000000..674ddac0e36 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/CoinbaseV2DigestCDP.java @@ -0,0 +1,115 @@ +package org.knowm.xchange.coinbase.cdp; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.knowm.xchange.service.BaseParamsDigest; +import si.mazi.rescu.RestInvocation; + +import java.io.StringReader; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Security; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +public class CoinbaseV2DigestCDP extends BaseParamsDigest { + + private final String cdpPrivateKey; + private final String cdpName; + + private CoinbaseV2DigestCDP(String cdpPrivateKey, String cdpName) { + super("nothing","HmacSHA256"); + this.cdpPrivateKey = cdpPrivateKey; + this.cdpName = cdpName; + } + + public static CoinbaseV2DigestCDP createInstance(String cdpPrivateKey, String cdpName) { + return (cdpPrivateKey == null || cdpName == null) ? null : new CoinbaseV2DigestCDP(cdpPrivateKey, cdpName); + } + + @Override + public String digestParams(RestInvocation restInvocation) { + String path = restInvocation.getInvocationUrl().replaceFirst("https://", "").replaceAll("\\?" + restInvocation.getQueryString(), ""); + String requestMethod = restInvocation.getHttpMethod(); + try { + return generateJwt(path, cdpPrivateKey, cdpName, requestMethod); + } catch (Exception e) { + throw new RuntimeException("Failed to generate JWT", e); + } + } + + public String generateJwt(String urlString, String cdpPrivateKey, String cdpName, String httpMethod) throws Exception { + // Register BouncyCastle as a security provider + Security.addProvider(new BouncyCastleProvider()); + + // Load environment variables + String privateKeyPEM = cdpPrivateKey.replace("\\n", "\n"); + String name = cdpName; + + // create header object + Map header = new HashMap<>(); + header.put("alg", "ES256"); + header.put("typ", "JWT"); + header.put("kid", name); + header.put("nonce", String.valueOf(Instant.now().getEpochSecond())); + + // create uri string for current request + String requestMethod = httpMethod; + String url = urlString; + String uri = requestMethod + " " + url; + + // create data object + Map data = new HashMap<>(); + data.put("iss", "cdp"); + data.put("nbf", Instant.now().getEpochSecond()); + data.put("exp", Instant.now().getEpochSecond() + 120); + data.put("sub", name); + data.put("uri", uri); + + // Load private key + PEMParser pemParser = new PEMParser(new StringReader(privateKeyPEM)); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + Object object = pemParser.readObject(); + PrivateKey privateKey; + + if (object instanceof PrivateKey) { + privateKey = (PrivateKey) object; + } else if (object instanceof org.bouncycastle.openssl.PEMKeyPair) { + privateKey = converter.getPrivateKey(((org.bouncycastle.openssl.PEMKeyPair) object).getPrivateKeyInfo()); + } else { + throw new Exception("Unexpected private key format"); + } + pemParser.close(); + + // Convert to ECPrivateKey + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); + ECPrivateKey ecPrivateKey = (ECPrivateKey) keyFactory.generatePrivate(keySpec); + + // create JWT + JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder(); + for (Map.Entry entry : data.entrySet()) { + claimsSetBuilder.claim(entry.getKey(), entry.getValue()); + } + JWTClaimsSet claimsSet = claimsSetBuilder.build(); + + JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES256).customParams(header).build(); + SignedJWT signedJWT = new SignedJWT(jwsHeader, claimsSet); + + JWSSigner signer = new ECDSASigner(ecPrivateKey); + signedJWT.sign(signer); + + String sJWT = signedJWT.serialize(); + return "Bearer " + sJWT; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseAccountServiceCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseAccountServiceCDP.java new file mode 100644 index 00000000000..c2d8eb8fdd5 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseAccountServiceCDP.java @@ -0,0 +1,109 @@ +package org.knowm.xchange.coinbase.cdp.service; + +import org.knowm.xchange.Exchange; +import org.knowm.xchange.coinbase.CoinbaseAdapters; +import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeHistoryParams; +import org.knowm.xchange.coinbase.v2.Coinbase; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData; +import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseBuySellResponse; +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.FundingRecord; +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; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class CoinbaseAccountServiceCDP extends CoinbaseAccountServiceRawCDP + implements AccountService { + + public CoinbaseAccountServiceCDP(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() { + return new CoinbaseTradeHistoryParams(); + } + + /** + * The Coinbase is not typical exchange. It has splitted buys and sells into wallets (accounts). + * To get it is necessary to know the accountId (wallet ID) see {@link AccountInfo#getWallets()} + */ + public List getWithdrawalHistory(CoinbaseTradeHistoryParams params, String accountId) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + final CoinbaseBuySellResponse withdrawals = + coinbase.getAllWithdrawals( + signatureCreator2, + accountId, + params.getLimit(), + params.getStartId() + ); + return CoinbaseAdapters.adaptFundings(withdrawals.getData()); + } + + /** + * The Coinbase is not typical exchange. It has splitted buys and sells into wallets (accounts). + * To get it is necessary to know the accountId (wallet ID) from {@link AccountInfo#getWallets()} + */ + public List getDepositHistory(CoinbaseTradeHistoryParams params, String accountId) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + final CoinbaseBuySellResponse deposits = + coinbase.getAllDeposits( + signatureCreator2, + accountId, + params.getLimit(), + params.getStartId() + ); + return CoinbaseAdapters.adaptFundings(deposits.getData()); + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseAccountServiceRawCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseAccountServiceRawCDP.java new file mode 100644 index 00000000000..1efb2d366c9 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseAccountServiceRawCDP.java @@ -0,0 +1,167 @@ +package org.knowm.xchange.coinbase.cdp.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeHistoryParams; +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.CoinbaseExpandTransactionsResponse; +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; + +import javax.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; + +public class CoinbaseAccountServiceRawCDP extends CoinbaseBaseServiceCDP { + + public CoinbaseAccountServiceRawCDP(Exchange exchange) { + super(exchange); + } + + public CoinbaseTransactionsResponse getTransactions(String accountId) throws IOException { + + return coinbase.getTransactions( + signatureCreator2, accountId); + } + + public CoinbaseExpandTransactionsResponse getExpandTransactions(String accountId, CoinbaseTradeHistoryParams params, String orderType) + throws IOException { + + return coinbase.getExpandedTransactions( + signatureCreator2, + accountId, + params.getLimit(), + orderType, + params.getStartId()); + } + + 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, accountId); + } + + 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, accountId); + } + + /** + * Authenticated resource that shows the current user accounts. + * + * @see developers.coinbase.com/api/v2#list-accounts + */ + public List getCoinbaseAccounts() throws IOException { + + List returnList = new ArrayList<>(); + List tmpList = null; + + String lastAccount = null; + do { + try { + tmpList = + coinbase + .getAccounts( + signatureCreator2, 100, lastAccount) + .getData(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + 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 { + + return coinbase + .getAccount( + signatureCreator2, + 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 { + + return coinbase + .getPaymentMethods(signatureCreator2) + .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/cdp/service/CoinbaseBaseServiceCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseBaseServiceCDP.java new file mode 100644 index 00000000000..5b459d5e937 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseBaseServiceCDP.java @@ -0,0 +1,104 @@ +package org.knowm.xchange.coinbase.cdp.service; + +import org.knowm.xchange.Exchange; +import org.knowm.xchange.client.ExchangeRestProxyBuilder; +import org.knowm.xchange.coinbase.service.CoinbaseDigest; +import org.knowm.xchange.coinbase.v3.CoinbaseAuthenticatedV3CDP; +import org.knowm.xchange.coinbase.v2.Coinbase; +import org.knowm.xchange.coinbase.cdp.CoinbaseAuthenticatedCDP; +import org.knowm.xchange.coinbase.cdp.CoinbaseV2DigestCDP; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseCurrencyData.CoinbaseCurrency; +import org.knowm.xchange.coinbase.v2.dto.marketdata.CoinbaseTimeData.CoinbaseTime; +import org.knowm.xchange.service.BaseExchangeService; +import org.knowm.xchange.service.BaseService; +import org.knowm.xchange.utils.DigestUtils; + +import javax.crypto.Mac; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class CoinbaseBaseServiceCDP extends BaseExchangeService implements BaseService { + + protected final CoinbaseAuthenticatedCDP coinbase; + protected final CoinbaseAuthenticatedV3CDP coinbaseV3; + protected final CoinbaseV2DigestCDP signatureCreator2; + + protected CoinbaseBaseServiceCDP(Exchange exchange) { + + super(exchange); + coinbase = + ExchangeRestProxyBuilder.forInterface( + CoinbaseAuthenticatedCDP.class, exchange.getExchangeSpecification()) + .build(); + + coinbaseV3 = + ExchangeRestProxyBuilder.forInterface( + CoinbaseAuthenticatedV3CDP.class, exchange.getExchangeSpecification()) + .build(); + + signatureCreator2 = + CoinbaseV2DigestCDP.createInstance(exchange.getExchangeSpecification().getSecretKey(), exchange.getExchangeSpecification().getApiKey()); + } + + /** + * 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 coinbase.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 coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData(); + } + + protected String getSignature(BigDecimal timestamp, HttpMethod method, String path, String body) { + String secretKey = exchange.getExchangeSpecification().getSecretKey(); + String message = timestamp + method.toString() + path + (body != null ? body : ""); + final Mac mac = CoinbaseDigest.createInstance(secretKey).getMac(); + byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); + 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); + 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/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseTradeServiceCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseTradeServiceCDP.java new file mode 100644 index 00000000000..fbee5f6b508 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseTradeServiceCDP.java @@ -0,0 +1,107 @@ +package org.knowm.xchange.coinbase.cdp.service; + +import org.knowm.xchange.Exchange; +import org.knowm.xchange.coinbase.CoinbaseAdapters; +import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseBuySellResponse; +import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeHistoryParams; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.account.AccountInfo; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.dto.trade.OpenOrders; +import org.knowm.xchange.dto.trade.StopOrder; +import org.knowm.xchange.dto.trade.UserTrades; +import org.knowm.xchange.exceptions.ExchangeException; +import org.knowm.xchange.exceptions.NotAvailableFromExchangeException; +import org.knowm.xchange.exceptions.NotYetImplementedForExchangeException; +import org.knowm.xchange.service.trade.TradeService; +import org.knowm.xchange.service.trade.params.CancelOrderParams; +import org.knowm.xchange.service.trade.params.TradeHistoryParams; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParams; + +import java.io.IOException; + +public final class CoinbaseTradeServiceCDP extends CoinbaseTradeServiceRawCDP implements TradeService { + + public CoinbaseTradeServiceCDP(Exchange exchange) { + super(exchange); + } + + /** + * ******************************************************************************************************************************************************** + */ + @Override + public OpenOrders getOpenOrders() throws NotAvailableFromExchangeException, IOException { + return getOpenOrders(createOpenOrdersParams()); + } + + @Override + public OpenOrders getOpenOrders(OpenOrdersParams params) + throws ExchangeException, NotAvailableFromExchangeException, + NotYetImplementedForExchangeException, IOException { + throw new NotAvailableFromExchangeException(); + } + + @Override + public String placeLimitOrder(LimitOrder limitOrder) throws NotAvailableFromExchangeException { + + throw new NotAvailableFromExchangeException(); + } + + @Override + public String placeStopOrder(StopOrder stopOrder) throws IOException { + throw new NotAvailableFromExchangeException(); + } + + @Override + public boolean cancelOrder(String orderId) throws NotAvailableFromExchangeException { + + throw new NotAvailableFromExchangeException(); + } + + @Override + public boolean cancelOrder(CancelOrderParams orderParams) + throws ExchangeException, NotAvailableFromExchangeException, + NotYetImplementedForExchangeException, IOException { + throw new NotAvailableFromExchangeException(); + } + + @Override + public TradeHistoryParams createTradeHistoryParams() { + return new CoinbaseTradeHistoryParams(); + } + + @Override + public OpenOrdersParams createOpenOrdersParams() { + return null; + } + + /** + * The Coinbase is not typical exchange. It has splitted buys and sells into wallets (accounts). + * To get it is necessary to know the accountId (wallet ID) see {@link AccountInfo#getWallets()} + */ + public UserTrades getBuyTradeHistory(CoinbaseTradeHistoryParams params, String accountId) + throws IOException { + final CoinbaseBuySellResponse buys = + coinbase.getBuys( + signatureCreator2, + accountId, + params.getLimit(), + params.getStartId()); + return CoinbaseAdapters.adaptTrades(buys.getData(), Order.OrderType.BID); + } + + /** + * The Coinbase is not typical exchange. It has splitted buys and sells into wallets (accounts). + * To get it is necessary to know the accountId (wallet ID) from {@link AccountInfo#getWallets()} + */ + public UserTrades getSellTradeHistory(CoinbaseTradeHistoryParams params, String accountId) + throws IOException { + final CoinbaseBuySellResponse sells = + coinbase.getSells( + signatureCreator2, + accountId, + params.getLimit(), + params.getStartId()); + return CoinbaseAdapters.adaptTrades(sells.getData(), Order.OrderType.ASK); + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseTradeServiceRawCDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseTradeServiceRawCDP.java new file mode 100644 index 00000000000..1598cb85840 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/cdp/service/CoinbaseTradeServiceRawCDP.java @@ -0,0 +1,182 @@ +package org.knowm.xchange.coinbase.cdp.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeHistoryParams; +import org.knowm.xchange.coinbase.v2.Coinbase; +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.v3.dto.transactions.CoinbaseAdvancedTradeAccountsResponse; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeFills; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeOrderFillsResponse; +import org.knowm.xchange.currency.Currency; + +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; + +class CoinbaseTradeServiceRawCDP extends CoinbaseBaseServiceCDP { + + protected CoinbaseTradeServiceRawCDP(Exchange exchange) { + super(exchange); + } + + /** + * Authenticated resource that lets you purchase Bitcoin using the primary bank account that is + * linked to your account. (You must link and verify your bank account through the website before + * this API call will work). The underlying optional parameter agree_btc_amount_varies is set to + * false. + * + * @see developers.coinbase.com/api/v2#place-buy-order + */ + public CoinbaseBuy buy(String accountId, BigDecimal total, Currency currency, boolean commit) + throws IOException { + + String path = "/v2/accounts/" + accountId + "/buys"; + String apiKey = exchange.getExchangeSpecification().getApiKey(); + BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + BuyPayload payload = new BuyPayload(total, currency.getCurrencyCode(), commit, false); + String body = new ObjectMapper().writeValueAsString(payload); + String signature = getSignature(timestamp, HttpMethod.POST, path, body); + + showCurl(HttpMethod.POST, apiKey, timestamp, signature, path, body); + + return coinbase + .buy( + MediaType.APPLICATION_JSON, + Coinbase.CB_VERSION_VALUE, + apiKey, + signature, + timestamp, + accountId, + payload) + .getData(); + } + + /** + * Authenticated resource that lets you convert Bitcoin crediting your primary bank account on + * Coinbase. (You must link and verify your bank account through the website before this API call + * will work). + * + * @see developers.coinbase.com/api/v2#place-sell-order + */ + public CoinbaseSell sell(String accountId, BigDecimal total, Currency currency, boolean commit) + throws IOException { + + return sellInternal( + accountId, new SellPayload(total, currency.getCurrencyCode(), commit, false)); + } + + /** + * + * @param params + * @return + * @throws IOException + */ + public List getAdvancedTradeOrderFills(CoinbaseTradeHistoryParams params) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + String start = params.getStartDatetime().toString(); + String end = params.getEndDateTime().toString(); + return coinbaseV3.getFills( + signatureCreator2, null, null, start, end, params.getLimit(), params.getCursor()) + .getFills(); + } + + /** + * + * @param params + * @return + * @throws IOException + */ + public CoinbaseAdvancedTradeOrderFillsResponse getAdvancedTradeOrderFillsRow(CoinbaseTradeHistoryParams params) + throws IOException { + String start = params.getStartDatetime().toString(); + String end = params.getEndDateTime().toString(); + return coinbaseV3.getFills( + signatureCreator2, null, null, start, end, params.getLimit(), params.getCursor()); + } + + /** + * + * @return + * @throws IOException + */ + public CoinbaseAdvancedTradeAccountsResponse getAdvancedTradeAccounts() throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + return coinbaseV3.getAccounts( + signatureCreator2, null, null); + } + + /** + * Authenticated resource that lets you convert Bitcoin crediting your primary bank account on + * Coinbase. (You must link and verify your bank account through the website before this API call + * will work). + * + * @see developers.coinbase.com/api/v2#place-sell-order + */ + public CoinbaseSell quote(String accountId, BigDecimal total, Currency currency) + throws IOException { + + return sellInternal(accountId, new SellPayload(total, currency.getCurrencyCode(), false, true)); + } + + private CoinbaseSell sellInternal(String accountId, SellPayload payload) throws IOException { + + String path = "/v2/accounts/" + accountId + "/sells"; + 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 + .sell( + MediaType.APPLICATION_JSON, + Coinbase.CB_VERSION_VALUE, + apiKey, + signature, + timestamp, + accountId, + payload) + .getData(); + } + + class BuyPayload extends AbstractPayload { + @JsonProperty String total; + + BuyPayload(BigDecimal total, String currency, boolean commit, boolean quote) { + super(currency, commit, quote); + this.total = total.toString(); + } + } + + class SellPayload extends AbstractPayload { + @JsonProperty String amount; + + SellPayload(BigDecimal amount, String currency, boolean commit, boolean quote) { + super(currency, commit, quote); + this.amount = amount.toString(); + } + } + + abstract class AbstractPayload { + @JsonProperty String currency; + @JsonProperty boolean commit; + @JsonProperty boolean quote; + + AbstractPayload(String currency, boolean commit, boolean quote) { + this.currency = currency; + this.commit = commit; + this.quote = quote; + } + } +} 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 a26857e17b4..74bcd032df8 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 @@ -11,11 +11,13 @@ import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Map; import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData; 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.CoinbaseExpandTransactionsResponse; 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; @@ -60,6 +62,19 @@ CoinbaseTransactionsResponse getTransactions( @PathParam("accountId") String accountId) throws IOException, CoinbaseException; + @GET + @Path("accounts/{accountId}/transactions") + CoinbaseExpandTransactionsResponse getExpandedTransactions( + @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, + @QueryParam("limit") int limit, + @QueryParam("order") String orderType, + @QueryParam("starting_after") String startingFrom) + throws IOException, CoinbaseException; + @GET @Path("accounts/{accountId}/buys") CoinbaseBuySellResponse getBuys( @@ -84,6 +99,30 @@ CoinbaseBuySellResponse getSells( @QueryParam("starting_after") String startingAfter) throws IOException, CoinbaseException; + @GET + @Path("accounts/{accountId}/deposits") + CoinbaseBuySellResponse getAllDeposits( + @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; + + @GET + @Path("accounts/{accountId}/withdrawals") + CoinbaseBuySellResponse getAllWithdrawals( + @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; + @GET @Path("accounts/{accountId}/deposits") Map getDeposits( 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..5ea465d82a1 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,14 +6,21 @@ 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.coinbase.cdp.service.CoinbaseAccountServiceCDP; +import org.knowm.xchange.coinbase.cdp.service.CoinbaseTradeServiceCDP; public class CoinbaseExchange extends BaseExchange implements Exchange { @Override protected void initServices() { this.marketDataService = new CoinbaseMarketDataService(this); - this.accountService = new CoinbaseAccountService(this); - this.tradeService = new CoinbaseTradeService(this); + if (exchangeSpecification.getApiKey().startsWith("organizations")){ + this.accountService = new CoinbaseAccountServiceCDP(this); + this.tradeService = new CoinbaseTradeServiceCDP(this); + } else { + this.accountService = new CoinbaseAccountService(this); + this.tradeService = new CoinbaseTradeService(this); + } } @Override 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..cd7262e0f5c 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,14 +1,17 @@ package org.knowm.xchange.coinbase.v2; -import static org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated.CB_ACCESS_TIMESTAMP; - -import jakarta.ws.rs.HeaderParam; import org.knowm.xchange.service.BaseParamsDigest; import org.knowm.xchange.utils.DigestUtils; import si.mazi.rescu.RestInvocation; +import javax.ws.rs.HeaderParam; + +import static org.knowm.xchange.coinbase.v2.CoinbaseAuthenticated.CB_ACCESS_TIMESTAMP; + public class CoinbaseV2Digest extends BaseParamsDigest { + public static final String ADVANCED_TRADING_V3 = "api/v3/brokerage/"; + private CoinbaseV2Digest(String secretKey) { super(secretKey, HMAC_SHA_256); } @@ -19,12 +22,14 @@ 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 message = timestamp + restInvocation.getHttpMethod() + pathWithQueryString; - + String path = restInvocation.getInvocationUrl(); + final String timestamp = restInvocation.getParamValue(HeaderParam.class, CB_ACCESS_TIMESTAMP).toString(); + if (path.contains(ADVANCED_TRADING_V3)) { + path = "/" + restInvocation.getPath(); + } else { + path = path.replace(restInvocation.getBaseUrl(), ""); + } + String message = timestamp + restInvocation.getHttpMethod() + path; return DigestUtils.bytesToHex(getMac().doFinal(message.getBytes())); } } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/CoinbaseUnitPrice.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/CoinbaseUnitPrice.java new file mode 100644 index 00000000000..c98908fee91 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/CoinbaseUnitPrice.java @@ -0,0 +1,30 @@ +package org.knowm.xchange.coinbase.v2.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.utils.Assert; + +import java.math.BigDecimal; + +@ToString +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class CoinbaseUnitPrice { + + private final Currency currency; + private final BigDecimal amount; + private final int scale; + + @JsonCreator + public CoinbaseUnitPrice( + @JsonProperty("amount") BigDecimal amount, @JsonProperty("currency") String currency, @JsonProperty("scale") int scale) { + this.amount = amount; + this.currency = Currency.getInstance(currency); + this.scale = scale; + } + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/CoinbaseExpandTransactionsResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/CoinbaseExpandTransactionsResponse.java new file mode 100644 index 00000000000..7104fcf0a41 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/CoinbaseExpandTransactionsResponse.java @@ -0,0 +1,20 @@ +package org.knowm.xchange.coinbase.v2.dto.account; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbasePagination; +import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseShowTransactionV2; + +import java.util.List; + +@Getter +public class CoinbaseExpandTransactionsResponse { + private final List data; + private final CoinbasePagination pagination; + + public CoinbaseExpandTransactionsResponse(@JsonProperty("pagination") CoinbasePagination pagination, + @JsonProperty("data") List data) { + this.data = data; + this.pagination = pagination; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseAdvancedTradeFill.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseAdvancedTradeFill.java new file mode 100644 index 00000000000..ddee60c8f33 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseAdvancedTradeFill.java @@ -0,0 +1,38 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class CoinbaseAdvancedTradeFill { + + private final String fillPrice; + private final String productId; + private final String orderId; + private final String commission; + private final String orderSide; + + public CoinbaseAdvancedTradeFill( + @JsonProperty("fill_price") String fillPrice, + @JsonProperty("product_id") String productId, + @JsonProperty("order_id") String orderId, + @JsonProperty("commission") String commission, + @JsonProperty("order_side") String orderSide) { + this.fillPrice = fillPrice; + this.productId = productId; + this.orderId = orderId; + this.commission = commission; + this.orderSide = orderSide; + } + + @Override + public String toString() { + return "{\"CoinbaseAdvancedTradeFill\":{" + + "\"fillPrice\":\"" + fillPrice + "\"" + + ", \"productId\":\"" + productId + "\"" + + ", \"orderId\":\"" + orderId + "\"" + + ", \"commission\":\"" + commission + "\"" + + ", \"orderSide\":\"" + orderSide + "\"" + + "}}"; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseDepositWithdrawalResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseDepositWithdrawalResponse.java new file mode 100644 index 00000000000..3e5f47d60b7 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseDepositWithdrawalResponse.java @@ -0,0 +1,14 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; + +@Getter +public class CoinbaseDepositWithdrawalResponse { + private final List data; + + public CoinbaseDepositWithdrawalResponse(@JsonProperty("data") List data) { + this.data = data; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbasePagination.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbasePagination.java new file mode 100644 index 00000000000..47dd0d023ba --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbasePagination.java @@ -0,0 +1,34 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +@ToString +@Getter +public class CoinbasePagination { + + + private final String endingBefore; + private final String startingAfter; + private final int limit; + private final String order; + private final String previousUri; + private final String nextUri; + + public CoinbasePagination( + @JsonProperty("ending_before") String endingBefore, + @JsonProperty("starting_after") String startingAfter, + @JsonProperty("limit") int limit, + @JsonProperty("order") String order, + @JsonProperty("previous_uri") String previousUri, + @JsonProperty("next_uri") String nextUri + ) { + this.endingBefore = endingBefore; + this.startingAfter = startingAfter; + this.limit = limit; + this.order = order; + this.previousUri = previousUri; + this.nextUri = nextUri; + } +} \ No newline at end of file diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseShowTransactionV2.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseShowTransactionV2.java new file mode 100644 index 00000000000..b5c32365937 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseShowTransactionV2.java @@ -0,0 +1,71 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; + +@ToString +@Getter +public class CoinbaseShowTransactionV2 { + private final String id; + private final String type; + private final String status; + private final CoinbaseAmount amount; + private final CoinbaseAmount nativeAmount; + private final String description; + private final String createdAt; + private final String resource; + private final String resourcePath; + private final CoinbaseAdvancedTradeFill advancedTradeFill; + private final CoinbaseTransactionV2NetworkField network; + private final CoinbaseTransactionV2ToField to; + private final CoinbaseTransactionV2Field from; + private final boolean cancelable; + private final String idem; + private final CoinbaseTransactionV2Expand buy; + private final CoinbaseTransactionV2Expand sell; + private final CoinbaseTrade trade; + + public CoinbaseShowTransactionV2( + @JsonProperty("id") String id, + @JsonProperty("type") String type, + @JsonProperty("status") String status, + @JsonProperty("amount") CoinbaseAmount amount, + @JsonProperty("native_amount") CoinbaseAmount nativeAmount, + @JsonProperty("description") String description, + @JsonProperty("created_at") String createdAt, + @JsonProperty("resource") String resource, + @JsonProperty("resource_path") String resourcePath, + @JsonProperty("advanced_trade_fill") CoinbaseAdvancedTradeFill advancedTradeFill, + @JsonProperty("network") CoinbaseTransactionV2NetworkField network, + @JsonProperty("to") CoinbaseTransactionV2ToField to, + @JsonProperty("from") CoinbaseTransactionV2Field from, + @JsonProperty("cancelable") boolean cancelable, + @JsonProperty("idem") String idem, + @JsonProperty("buy") CoinbaseTransactionV2Expand buy, + @JsonProperty("sell") CoinbaseTransactionV2Expand sell, + @JsonProperty("trade") CoinbaseTrade trade + ) + { + this.id = id; + this.type = type; + this.status = status; + this.amount = amount; + this.nativeAmount = nativeAmount; + this.description = description; + this.createdAt = createdAt; + this.resource = resource; + this.resourcePath = resourcePath; + this.advancedTradeFill = advancedTradeFill; + this.network = network; + this.to = to; + this.from = from; + this.idem = idem; + this.buy = buy; + this.sell = sell; + this.trade = trade; + this.cancelable = cancelable; + } + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTrade.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTrade.java new file mode 100644 index 00000000000..75c9bd56270 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTrade.java @@ -0,0 +1,46 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class CoinbaseTrade { + + private final String id; + private final String paymentMethodName; + private final String resource; + private final String resourcePath; + + public CoinbaseTrade( + @JsonProperty("id") String id, + @JsonProperty("payment_method_name") String paymentMethodName, + @JsonProperty("resource") String resource, + @JsonProperty("resource_path") String resourcePath) { + this.id = id; + this.paymentMethodName = paymentMethodName; + this.resource = resource; + this.resourcePath = resourcePath; + } + + @Override + public String toString() { + return "{" + + "\"id\":" + + '\"' + + id + + '\"' + + ",\"paymentMethodName\":" + + '\"' + + paymentMethodName + + '\"' + + ",\"resource\":" + + '\"' + + resource + + '\"' + + ",\"resourcePath\":" + + '\"' + + resourcePath + + '\"' + + '}'; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionDetails.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionDetails.java new file mode 100644 index 00000000000..fc2de0462eb --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionDetails.java @@ -0,0 +1,38 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class CoinbaseTransactionDetails { + private final String title; + private final String subtitle; + private final String paymentMethodName; + + public CoinbaseTransactionDetails( + @JsonProperty("title") String title, + @JsonProperty("subtitle") String subtitle, + @JsonProperty("payment_method_name") String paymentMethodName) { + this.title = title; + this.subtitle = subtitle; + this.paymentMethodName = paymentMethodName; + } + + @Override + public String toString() { + return "{" + + "\"title\":" + + '\"' + + title + + '\"' + + ",\"subtitle\":" + + '\"' + + subtitle + + '\"' + + ",\"paymentMethodName\":" + + '\"' + + paymentMethodName + + '\"' + + "}"; + } +} \ No newline at end of file diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2Expand.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2Expand.java new file mode 100644 index 00000000000..d4164a9bcc6 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2Expand.java @@ -0,0 +1,47 @@ +package org.knowm.xchange.coinbase.v2.dto.account.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@FieldDefaults(level = PROTECTED) +public class CoinbaseTransactionV2Expand { + + private final String id; + private final String paymentMethodName; + private final CoinbaseAmount fee; + private final CoinbaseAmount total; + private final CoinbaseAmount subtotal; + private final String idem; + + public CoinbaseTransactionV2Expand( + @JsonProperty("id") String id, + @JsonProperty("payment_method_name") String paymentMethodName, + @JsonProperty("fee") CoinbaseAmount fee, + @JsonProperty("total") CoinbaseAmount total, + @JsonProperty("subtotal") CoinbaseAmount subtotal, + @JsonProperty("idem") String idem) { + this.id = id; + this.paymentMethodName = paymentMethodName; + this.fee = fee; + this.total = total; + this.subtotal = subtotal; + this.idem = idem; + } + + @Override + public String toString() { + return "{\"CoinbaseTransactionV2Expand\":{" + + "\"id\":\"" + id + "\"" + + ", \"paymentMethodName\":\"" + paymentMethodName + "\"" + + ", \"fee\":" + fee + + ", \"total\":" + total + + ", \"subtotal\":" + subtotal + + ", \"idem\":\"" + idem + "\"" + + "}}"; + } +} \ No newline at end of file diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2NetworkField.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2NetworkField.java index 0aa284dbd5a..95624ac56f2 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2NetworkField.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/dto/account/transactions/CoinbaseTransactionV2NetworkField.java @@ -2,14 +2,26 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; @Getter public class CoinbaseTransactionV2NetworkField { - private String status; + private final String status; + private final String networkName; + private final CoinbaseAmount transactionFee; + private final String hash; - public CoinbaseTransactionV2NetworkField(@JsonProperty("status") String status) { + public CoinbaseTransactionV2NetworkField( + @JsonProperty("status") String status, + @JsonProperty("network_name") String networkName, + @JsonProperty("transaction_fee") CoinbaseAmount transactionFee, + @JsonProperty("hash") String hash + ) { this.status = status; + this.networkName = networkName; + this.transactionFee = transactionFee; + this.hash = hash; } @Override diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountService.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountService.java index 468b318f8e8..3a5ef41f9c5 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountService.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseAccountService.java @@ -1,15 +1,15 @@ package org.knowm.xchange.coinbase.v2.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.CoinbaseAdapters; +import org.knowm.xchange.coinbase.v2.Coinbase; import org.knowm.xchange.coinbase.v2.dto.CoinbaseAmount; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData; +import org.knowm.xchange.coinbase.v2.dto.account.transactions.CoinbaseBuySellResponse; 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.FundingRecord; import org.knowm.xchange.dto.account.Wallet; import org.knowm.xchange.exceptions.ExchangeException; import org.knowm.xchange.exceptions.NotAvailableFromExchangeException; @@ -19,6 +19,12 @@ import org.knowm.xchange.service.trade.params.TradeHistoryParams; import org.knowm.xchange.service.trade.params.WithdrawFundsParams; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + public final class CoinbaseAccountService extends CoinbaseAccountServiceRaw implements AccountService { @@ -63,6 +69,48 @@ public String withdrawFunds(WithdrawFundsParams params) @Override public TradeHistoryParams createFundingHistoryParams() { - throw new NotAvailableFromExchangeException(); + return new CoinbaseTradeHistoryParams(); + } + + /** + * The Coinbase is not typical exchange. It has splitted buys and sells into wallets (accounts). + * To get it is necessary to know the accountId (wallet ID) see {@link AccountInfo#getWallets()} + */ + public List getWithdrawalHistory(CoinbaseTradeHistoryParams params, String accountId) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + final CoinbaseBuySellResponse withdrawals = + coinbase.getAllWithdrawals( + Coinbase.CB_VERSION_VALUE, + apiKey, + signatureCreator2, + timestamp, + accountId, + params.getLimit(), + params.getStartId() + ); + return CoinbaseAdapters.adaptFundings(withdrawals.getData()); + } + + /** + * The Coinbase is not typical exchange. It has splitted buys and sells into wallets (accounts). + * To get it is necessary to know the accountId (wallet ID) from {@link AccountInfo#getWallets()} + */ + public List getDepositHistory(CoinbaseTradeHistoryParams params, String accountId) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + final CoinbaseBuySellResponse deposits = + coinbase.getAllDeposits( + Coinbase.CB_VERSION_VALUE, + apiKey, + signatureCreator2, + timestamp, + accountId, + params.getLimit(), + params.getStartId() + ); + return CoinbaseAdapters.adaptFundings(deposits.getData()); } } 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 c16660ba2ea..c2f71312ef1 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 @@ -2,20 +2,22 @@ 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.Coinbase; import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseAccountData.CoinbaseAccount; +import org.knowm.xchange.coinbase.v2.dto.account.CoinbaseExpandTransactionsResponse; 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; +import javax.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; + public class CoinbaseAccountServiceRaw extends CoinbaseBaseService { public CoinbaseAccountServiceRaw(Exchange exchange) { @@ -30,6 +32,21 @@ public CoinbaseTransactionsResponse getTransactions(String accountId) throws IOE Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, accountId); } + public CoinbaseExpandTransactionsResponse getExpandTransactions(String accountId, CoinbaseTradeHistoryParams params, String orderType) + throws IOException { + + String apiKey = exchange.getExchangeSpecification().getApiKey(); + BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + + return coinbase.getExpandedTransactions( + Coinbase.CB_VERSION_VALUE, apiKey, + signatureCreator2, timestamp, + accountId, + params.getLimit(), + orderType, + params.getStartId()); + } + public Map getDeposits(String accountId) throws IOException { String apiKey = exchange.getExchangeSpecification().getApiKey(); BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); 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..6d921e1701f 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 @@ -1,11 +1,5 @@ package org.knowm.xchange.coinbase.v2.service; -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.util.List; -import javax.crypto.Mac; import org.knowm.xchange.Exchange; import org.knowm.xchange.client.ExchangeRestProxyBuilder; import org.knowm.xchange.coinbase.service.CoinbaseDigest; @@ -14,13 +8,22 @@ 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.CoinbaseTimeData.CoinbaseTime; +import org.knowm.xchange.coinbase.v3.CoinbaseAuthenticatedV3; import org.knowm.xchange.service.BaseExchangeService; import org.knowm.xchange.service.BaseService; import org.knowm.xchange.utils.DigestUtils; +import javax.crypto.Mac; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; + public class CoinbaseBaseService extends BaseExchangeService implements BaseService { protected final CoinbaseAuthenticated coinbase; + protected final CoinbaseAuthenticatedV3 coinbaseV3; protected final CoinbaseV2Digest signatureCreator2; protected CoinbaseBaseService(Exchange exchange) { @@ -31,6 +34,11 @@ protected CoinbaseBaseService(Exchange exchange) { CoinbaseAuthenticated.class, exchange.getExchangeSpecification()) .build(); + coinbaseV3 = + ExchangeRestProxyBuilder.forInterface( + CoinbaseAuthenticatedV3.class, exchange.getExchangeSpecification()) + .build(); + signatureCreator2 = CoinbaseV2Digest.createInstance(exchange.getExchangeSpecification().getSecretKey()); } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeHistoryParams.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeHistoryParams.java index 7153f3fb58c..55e1493d63f 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeHistoryParams.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeHistoryParams.java @@ -4,11 +4,16 @@ import org.knowm.xchange.service.trade.params.TradeHistoryParamLimit; import org.knowm.xchange.service.trade.params.TradeHistoryParamsIdSpan; +import java.time.Instant; + public class CoinbaseTradeHistoryParams implements TradeHistoryParamsIdSpan, TradeHistoryParamLimit { private String startId; private Integer limit; + private Instant startDatetime; + private Instant endDateTime; + private String cursor; @Override public Integer getLimit() { @@ -39,4 +44,28 @@ public String getEndId() { public void setEndId(String endId) { throw new NotAvailableFromExchangeException("Coinbase does not support ending transaction ID."); } + + public Instant getStartDatetime() { + return startDatetime; + } + + public void setStartDatetime(Instant startDatetime) { + this.startDatetime = startDatetime; + } + + public Instant getEndDateTime() { + return endDateTime; + } + + public void setEndDateTime(Instant endDateTime) { + this.endDateTime = endDateTime; + } + + public String getCursor() { + return cursor; + } + + public void setCursor(String cursor) { + this.cursor = cursor; + } } diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeServiceRaw.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeServiceRaw.java index b20a8f5f1eb..e162517f363 100644 --- a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeServiceRaw.java +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/CoinbaseTradeServiceRaw.java @@ -2,15 +2,20 @@ 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 org.knowm.xchange.Exchange; import org.knowm.xchange.coinbase.v2.Coinbase; 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.v3.dto.transactions.CoinbaseAdvancedTradeAccountsResponse; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeFills; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeOrderFillsResponse; import org.knowm.xchange.currency.Currency; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; + class CoinbaseTradeServiceRaw extends CoinbaseBaseService { protected CoinbaseTradeServiceRaw(Exchange exchange) { @@ -65,6 +70,52 @@ public CoinbaseSell sell(String accountId, BigDecimal total, Currency currency, accountId, new SellPayload(total, currency.getCurrencyCode(), commit, false)); } + /** + * + * @param params + * @return + * @throws IOException + */ + public List getAdvancedTradeOrderFills(CoinbaseTradeHistoryParams params) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + String start = params.getStartDatetime().toString(); + String end = params.getEndDateTime().toString(); + final CoinbaseAdvancedTradeOrderFillsResponse fills = coinbaseV3.getFills(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, timestamp, null + , null, start, end, params.getLimit(), params.getCursor()); + return fills.getFills(); + } + + /** + * + * @param params + * @return + * @throws IOException + */ + public CoinbaseAdvancedTradeOrderFillsResponse getAdvancedTradeOrderFillsRow(CoinbaseTradeHistoryParams params) + throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + String start = params.getStartDatetime().toString(); + String end = params.getEndDateTime().toString(); + return coinbaseV3.getFills(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, + timestamp, null, null, start, end, params.getLimit(), params.getCursor()); + } + + /** + * + * @return + * @throws IOException + */ + public CoinbaseAdvancedTradeAccountsResponse getAdvancedTradeAccounts() throws IOException { + final String apiKey = exchange.getExchangeSpecification().getApiKey(); + final BigDecimal timestamp = coinbase.getTime(Coinbase.CB_VERSION_VALUE).getData().getEpoch(); + final CoinbaseAdvancedTradeAccountsResponse accounts = coinbaseV3.getAccounts(Coinbase.CB_VERSION_VALUE, apiKey, signatureCreator2, + timestamp, null, null); + return accounts; + } + /** * Authenticated resource that lets you convert Bitcoin crediting your primary bank account on * Coinbase. (You must link and verify your bank account through the website before this API call diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/TransactionType.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/TransactionType.java new file mode 100644 index 00000000000..c96bc268f9f --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v2/service/TransactionType.java @@ -0,0 +1,18 @@ +package org.knowm.xchange.coinbase.v2.service; + +public enum TransactionType { + BUY("buy"), + SELL("sell"), + RECEIVE("receive"), + SEND("send"); + + String name; + + TransactionType(String name) { + this.name = name; + } + + String getName() { + return name; + } +} \ No newline at end of file diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticatedV3.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticatedV3.java new file mode 100644 index 00000000000..9fa07da4ba0 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticatedV3.java @@ -0,0 +1,74 @@ +package org.knowm.xchange.coinbase.v3; + +import org.knowm.xchange.coinbase.v2.Coinbase; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeAccountsResponse; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeOrderFillsResponse; +import si.mazi.rescu.ParamsDigest; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.math.BigDecimal; + +import static org.knowm.xchange.coinbase.v2.CoinbaseV2Digest.ADVANCED_TRADING_V3; + +@Path(ADVANCED_TRADING_V3) +@Produces(MediaType.APPLICATION_JSON) +public interface CoinbaseAuthenticatedV3 extends Coinbase { + + /** + * All API key requests must be signed and contain the following headers. + * + *

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 + * 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 + * GET requests). + * + *

The method should be UPPER CASE. + * + *

developers.coinbase.com/api/v2#api-key + */ + String CB_ACCESS_KEY = "CB-ACCESS-KEY"; + + String CB_ACCESS_SIGN = "CB-ACCESS-SIGN"; + String CB_ACCESS_TIMESTAMP = "CB-ACCESS-TIMESTAMP"; + + // ok + @GET + @Path("accounts") + CoinbaseAdvancedTradeAccountsResponse getAccounts( + @HeaderParam(CB_VERSION) String apiVersion, + @HeaderParam(CB_ACCESS_KEY) String apiKey, + @HeaderParam(CB_ACCESS_SIGN) ParamsDigest signature, + @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, + @QueryParam("limit") Integer limit, + @QueryParam("cursor") String cursor) + throws IOException, CoinbaseException; + + @GET + @Path("orders/historical/fills") + CoinbaseAdvancedTradeOrderFillsResponse getFills( + @HeaderParam(CB_VERSION) String apiVersion, + @HeaderParam(CB_ACCESS_KEY) String apiKey, + @HeaderParam(CB_ACCESS_SIGN) ParamsDigest signature, + @HeaderParam(CB_ACCESS_TIMESTAMP) BigDecimal timestamp, + @QueryParam("order_id") String orderId, + @QueryParam("product_id") String productId, + @QueryParam("start_sequence_timestamp") String start, + @QueryParam("end_sequence_timestamp") String end, + @QueryParam("limit") int limit, + @QueryParam("cursor") String cursor + ) + throws IOException, CoinbaseException; + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticatedV3CDP.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticatedV3CDP.java new file mode 100644 index 00000000000..930c11ca3a6 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/CoinbaseAuthenticatedV3CDP.java @@ -0,0 +1,63 @@ +package org.knowm.xchange.coinbase.v3; + +import org.knowm.xchange.coinbase.v2.Coinbase; +import org.knowm.xchange.coinbase.v2.dto.CoinbaseException; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeAccountsResponse; +import org.knowm.xchange.coinbase.v3.dto.transactions.CoinbaseAdvancedTradeOrderFillsResponse; +import si.mazi.rescu.ParamsDigest; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import java.io.IOException; + +import static org.knowm.xchange.coinbase.v2.CoinbaseV2Digest.ADVANCED_TRADING_V3; + +@Path(ADVANCED_TRADING_V3) +@Produces(MediaType.APPLICATION_JSON) +public interface CoinbaseAuthenticatedV3CDP extends Coinbase { + + /** + * All API key requests must be signed and contain the following headers. + * + *

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 + * 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 + * GET requests). + * + *

The method should be UPPER CASE. + * + *

developers.coinbase.com/api/v2#api-key + */ + + // ok + @GET + @Path("accounts") + CoinbaseAdvancedTradeAccountsResponse getAccounts( + @HeaderParam("Authorization") ParamsDigest signature, + @QueryParam("limit") Integer limit, + @QueryParam("cursor") String cursor) + throws IOException, CoinbaseException; + + @GET + @Path("orders/historical/fills") + CoinbaseAdvancedTradeOrderFillsResponse getFills( + @HeaderParam("Authorization") ParamsDigest signature, + @QueryParam("order_id") String orderId, + @QueryParam("product_id") String productId, + @QueryParam("start_sequence_timestamp") String start, + @QueryParam("end_sequence_timestamp") String end, + @QueryParam("limit") int limit, + @QueryParam("cursor") String cursor + ) + throws IOException, CoinbaseException; + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeAccountsResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeAccountsResponse.java new file mode 100644 index 00000000000..9ddd0c392c8 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeAccountsResponse.java @@ -0,0 +1,25 @@ +package org.knowm.xchange.coinbase.v3.dto.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.util.List; + +@Getter +public class CoinbaseAdvancedTradeAccountsResponse { + public final List accounts; + private final boolean hasNext; + private final String cursor; + private final String size; + + public CoinbaseAdvancedTradeAccountsResponse(@JsonProperty("accounts") List accounts, + @JsonProperty( + "has_next") boolean hasNext, @JsonProperty("cursor") String cursor, @JsonProperty("size") String size + ) { + this.accounts = accounts; + this.hasNext = hasNext; + this.cursor = cursor; + this.size = size; + } + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeFills.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeFills.java new file mode 100644 index 00000000000..41a2bac67ca --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeFills.java @@ -0,0 +1,61 @@ +package org.knowm.xchange.coinbase.v3.dto.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.experimental.FieldDefaults; + +import java.math.BigDecimal; + +import static lombok.AccessLevel.PRIVATE; + +@Getter +@FieldDefaults(level = PRIVATE, makeFinal = true) +public class CoinbaseAdvancedTradeFills { + + String entryId; + String tradeId; + String orderId; + String tradeTime; + String tradeType; + BigDecimal price; + BigDecimal size; + BigDecimal commission; + String productId; + String sequenceTimestamp; + String liquidityIndicator; + String sizeInQuote; + String userId; + String side; + + public CoinbaseAdvancedTradeFills( + @JsonProperty("entry_id") String entryId, + @JsonProperty("trade_id") String tradeId, + @JsonProperty("order_id") String orderId, + @JsonProperty("trade_time") String tradeTime, + @JsonProperty("trade_type") String tradeType, + @JsonProperty("price") BigDecimal price, + @JsonProperty("size") BigDecimal size, + @JsonProperty("commission") BigDecimal commission, + @JsonProperty("product_id") String productId, + @JsonProperty("sequence_timestamp") String sequenceTimestamp, + @JsonProperty("liquidity_indicator") String liquidityIndicator, + @JsonProperty("size_in_quote") String sizeInQuote, + @JsonProperty("user_id") String userId, + @JsonProperty("side") String side) { + + this.entryId = entryId; + this.tradeId = tradeId; + this.orderId = orderId; + this.tradeTime = tradeTime; + this.tradeType = tradeType; + this.price = price; + this.size = size; + this.commission = commission; + this.productId = productId; + this.sequenceTimestamp = sequenceTimestamp; + this.liquidityIndicator = liquidityIndicator; + this.sizeInQuote = sizeInQuote; + this.userId = userId; + this.side = side; + } +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeOrderFillsResponse.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeOrderFillsResponse.java new file mode 100644 index 00000000000..eb5d6a58d57 --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradeOrderFillsResponse.java @@ -0,0 +1,19 @@ +package org.knowm.xchange.coinbase.v3.dto.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.util.List; + +@Getter +public class CoinbaseAdvancedTradeOrderFillsResponse { + private final List fills; + private final String cursor; + + public CoinbaseAdvancedTradeOrderFillsResponse(@JsonProperty("fills") List fills, @JsonProperty("cursor") String cursor ) { + this.fills = fills; + this.cursor = cursor; + + } + +} diff --git a/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradesAccounts.java b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradesAccounts.java new file mode 100644 index 00000000000..b803775389b --- /dev/null +++ b/xchange-coinbase/src/main/java/org/knowm/xchange/coinbase/v3/dto/transactions/CoinbaseAdvancedTradesAccounts.java @@ -0,0 +1,54 @@ +package org.knowm.xchange.coinbase.v3.dto.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.experimental.FieldDefaults; + +import static lombok.AccessLevel.PRIVATE; + +@Getter +@FieldDefaults(level = PRIVATE, makeFinal = true) +public class CoinbaseAdvancedTradesAccounts { + + String uuid; + String name; + String currency; + Object availableBalance; + boolean def; + boolean active; + String createdAt; + String updatedAt; + String deletedAt; + String type; + boolean ready; + Object hold; + + public CoinbaseAdvancedTradesAccounts( + @JsonProperty("uuid") String uuid, + @JsonProperty("name") String name, + @JsonProperty("currency") String currency, + @JsonProperty("available_balance") Object availableBalance, + @JsonProperty("default") boolean def, + @JsonProperty("active") boolean active, + @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, + @JsonProperty("deleted_at") String deletedAt, + @JsonProperty("type") String type, + @JsonProperty("ready") boolean ready, + @JsonProperty("hold") Object hold + ) { + + this.uuid = uuid; + this.name = name; + this.currency = currency; + this.availableBalance = availableBalance; + this.def = def; + this.active = active; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.deletedAt = deletedAt; + this.type = type; + this.ready = ready; + this.hold = hold; + } +}