From a0efec1879a98d3d3220fbfb2b5f5f145aa79884 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 20 May 2024 22:56:53 +0200 Subject: [PATCH 01/13] [general] Define log4j-api version --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index 9d370c4b26f..90dad44ce7c 100644 --- a/pom.xml +++ b/pom.xml @@ -353,6 +353,12 @@ ${version.qos.logback} + + org.slf4j + slf4j-api + ${version.slf4j} + + com.auth0 From ab7bf44195b274f2de7ace4a5aab868a1495b808 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 20 May 2024 23:23:41 +0200 Subject: [PATCH 02/13] [coinex] Add http requests --- xchange-coinex/.gitignore | 1 + xchange-coinex/README.md | 11 +++++++++++ .../example.http-client.private.env.json | 6 ++++++ xchange-coinex/http-client.env.json | 5 +++++ .../src/test/resources/rest/asset.v2.http | 11 +++++++++++ .../src/test/resources/rest/market.v1.http | 12 ++++++++++++ xchange-coinex/src/test/resources/rest/sign.js | 16 ++++++++++++++++ .../src/test/resources/rest/spot.v2.http | 16 ++++++++++++++++ 8 files changed, 78 insertions(+) create mode 100644 xchange-coinex/.gitignore create mode 100644 xchange-coinex/README.md create mode 100644 xchange-coinex/example.http-client.private.env.json create mode 100644 xchange-coinex/http-client.env.json create mode 100644 xchange-coinex/src/test/resources/rest/asset.v2.http create mode 100644 xchange-coinex/src/test/resources/rest/market.v1.http create mode 100644 xchange-coinex/src/test/resources/rest/sign.js create mode 100644 xchange-coinex/src/test/resources/rest/spot.v2.http diff --git a/xchange-coinex/.gitignore b/xchange-coinex/.gitignore new file mode 100644 index 00000000000..42c7d2cd0eb --- /dev/null +++ b/xchange-coinex/.gitignore @@ -0,0 +1 @@ +http-client.private.env.json \ No newline at end of file diff --git a/xchange-coinex/README.md b/xchange-coinex/README.md new file mode 100644 index 00000000000..6f4d4742fa7 --- /dev/null +++ b/xchange-coinex/README.md @@ -0,0 +1,11 @@ +## Using IntelliJ Idea HTTP client + +There are *.http files stored in `src/test/resources/rest` that can be used with IntelliJ Idea HTTP Client. + +Some requests need authorization, so the api credentials have to be stored in `http-client.private.env.json` in module's root. Sample content can be found in `example.http-client.private.env.json` + +> [!CAUTION] +> Never commit your api credentials to the repository! + + +[HTTP Client documentation](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) \ No newline at end of file diff --git a/xchange-coinex/example.http-client.private.env.json b/xchange-coinex/example.http-client.private.env.json new file mode 100644 index 00000000000..977c974ecbe --- /dev/null +++ b/xchange-coinex/example.http-client.private.env.json @@ -0,0 +1,6 @@ +{ + "default": { + "api_key": "replace_me", + "api_secret": "replace_me" + } +} \ No newline at end of file diff --git a/xchange-coinex/http-client.env.json b/xchange-coinex/http-client.env.json new file mode 100644 index 00000000000..a85ba9d9d8e --- /dev/null +++ b/xchange-coinex/http-client.env.json @@ -0,0 +1,5 @@ +{ + "default": { + "api_host": "https://api.coinex.com", + } +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/rest/asset.v2.http b/xchange-coinex/src/test/resources/rest/asset.v2.http new file mode 100644 index 00000000000..b14f30adc92 --- /dev/null +++ b/xchange-coinex/src/test/resources/rest/asset.v2.http @@ -0,0 +1,11 @@ +### Get Balance in Spot Account +< {% + import {gen_sign} from 'sign.js' + gen_sign("GET", request); +%} +GET {{api_host}}/v2/assets/spot/balance +X-COINEX-KEY: {{api_key}} +X-COINEX-TIMESTAMP: {{timestamp}} +X-COINEX-SIGN: {{sign}} + + diff --git a/xchange-coinex/src/test/resources/rest/market.v1.http b/xchange-coinex/src/test/resources/rest/market.v1.http new file mode 100644 index 00000000000..99bdbd80bb7 --- /dev/null +++ b/xchange-coinex/src/test/resources/rest/market.v1.http @@ -0,0 +1,12 @@ +### Acquire Single Market Statistics +GET {{api_host}}/v1/market/ticker?market=BTCUSDT + + +### Acquire All Market Statistics +GET {{api_host}}/v1/market/ticker/all + + +### Acquire Asset Allocation +GET {{api_host}}/v1/common/asset/config + + diff --git a/xchange-coinex/src/test/resources/rest/sign.js b/xchange-coinex/src/test/resources/rest/sign.js new file mode 100644 index 00000000000..0d42bda1c54 --- /dev/null +++ b/xchange-coinex/src/test/resources/rest/sign.js @@ -0,0 +1,16 @@ +export function gen_sign(method, request) { + const pattern = RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"); + const url = request.url.tryGetSubstituted(); + const matches = url.match(pattern); + + const path = matches[5]; + const query = matches[7] || ""; + const body = request.body.tryGetSubstituted() || ""; + const timestamp = Math.floor(Date.now()).toFixed(); + const payloadToSign = `${method}${path}${query}${body}${timestamp}`; + const apiSecret = request.environment.get("api_secret"); + const sign = crypto.hmac.sha256().withTextSecret(apiSecret).updateWithText(payloadToSign).digest().toHex(); + + request.variables.set("timestamp", timestamp); + request.variables.set("sign", sign); +} \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/rest/spot.v2.http b/xchange-coinex/src/test/resources/rest/spot.v2.http new file mode 100644 index 00000000000..c70cc577d8d --- /dev/null +++ b/xchange-coinex/src/test/resources/rest/spot.v2.http @@ -0,0 +1,16 @@ +### Get Market Status +GET {{api_host}}/v2/spot/market + + +### Get Market Information +GET {{api_host}}/v2/spot/ticker + + +### Get Market Index +GET {{api_host}}/v2/spot/index + + +### Get Market Depth +GET {{api_host}}/v2/spot/depth + + From f2580247795de9d67c19367f23b7d1a59205b237 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 20 May 2024 23:24:08 +0200 Subject: [PATCH 03/13] [coinex] Tune lombok and maven settings --- xchange-coinex/lombok.config | 2 + xchange-coinex/pom.xml | 77 ++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 xchange-coinex/lombok.config diff --git a/xchange-coinex/lombok.config b/xchange-coinex/lombok.config new file mode 100644 index 00000000000..22b090cc4b6 --- /dev/null +++ b/xchange-coinex/lombok.config @@ -0,0 +1,2 @@ +lombok.equalsAndHashCode.callSuper = call +lombok.tostring.callsuper = call \ No newline at end of file diff --git a/xchange-coinex/pom.xml b/xchange-coinex/pom.xml index c8991747ef3..fcdb4e55bc8 100644 --- a/xchange-coinex/pom.xml +++ b/xchange-coinex/pom.xml @@ -1,11 +1,11 @@ + 4.0.0 - xchange-parent org.knowm.xchange + xchange-parent 5.1.2-SNAPSHOT - 4.0.0 xchange-coinex @@ -20,14 +20,85 @@ http://knowm.org/open-source/xchange/ + + 3.2.1 + 3.2.0 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${version.fasterxml} + + + + org.junit.jupiter + junit-jupiter-engine + test + + org.knowm.xchange xchange-core ${project.version} + + + org.mockito + mockito-junit-jupiter + test + + + + + + com.github.ekryd.sortpom + sortpom-maven-plugin + ${version.sortpom-maven-plugin} + + true + false + groupId,artifactId + true + 4 + groupId,artifactId + false + + + + + sort + + verify + + + + + + maven-enforcer-plugin + ${version.maven-enforcer-plugin} + + + enforce + + enforce + + + + + + + + + + + + + + - \ No newline at end of file + From 92f476523ace180303c3799bf306a8a217e2cb65 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 20 May 2024 23:24:26 +0200 Subject: [PATCH 04/13] [coinex] Configure ObjectMapper --- .../CoinexJacksonObjectMapperFactory.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java new file mode 100644 index 00000000000..a69e773bf92 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/CoinexJacksonObjectMapperFactory.java @@ -0,0 +1,20 @@ +package org.knowm.xchange.coinex.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import si.mazi.rescu.serialization.jackson.DefaultJacksonObjectMapperFactory; + +public class CoinexJacksonObjectMapperFactory extends DefaultJacksonObjectMapperFactory { + + @Override + public void configureObjectMapper(ObjectMapper objectMapper) { + super.configureObjectMapper(objectMapper); + + // by default read timetamps as milliseconds + objectMapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + + // enable parsing to Instant + objectMapper.registerModule(new JavaTimeModule()); + } +} From 585df9372307b3042ca33fa7328dfa5ad5e70cd1 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 20 May 2024 23:29:34 +0200 Subject: [PATCH 05/13] [coinex] Extend market data, add error handling, symbol mappings --- .../java/org/knowm/xchange/coinex/Coinex.java | 40 ++++++++ .../knowm/xchange/coinex/CoinexAdapters.java | 96 +++++++++++++++---- .../xchange/coinex/CoinexErrorAdapter.java | 24 +++++ .../knowm/xchange/coinex/CoinexExchange.java | 35 +++++-- .../converter/StringToCurrencyConverter.java | 13 +++ .../xchange/coinex/dto/CoinexException.java | 18 ++++ .../xchange/coinex/dto/CoinexResponse.java | 31 +++--- .../CoinexAllMarketStatisticsV1.java | 21 ++++ .../marketdata/CoinexCurrencyPairInfo.java | 49 ++++++++++ .../CoinexSingleMarketStatisticsV1.java | 20 ++++ .../coinex/dto/marketdata/CoinexTickerV1.java | 48 ++++++++++ .../service/CoinexMarketDataService.java | 77 +++++++++++++++ .../service/CoinexMarketDataServiceRaw.java | 36 +++++++ .../CoinexMarketDataServiceIntegration.java | 45 +++++++++ 14 files changed, 506 insertions(+), 47 deletions(-) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyConverter.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexException.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexAllMarketStatisticsV1.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexSingleMarketStatisticsV1.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexTickerV1.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java create mode 100644 xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceIntegration.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java new file mode 100644 index 00000000000..69737244d19 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java @@ -0,0 +1,40 @@ +package org.knowm.xchange.coinex; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.List; +import org.knowm.xchange.coinex.dto.CoinexException; +import org.knowm.xchange.coinex.dto.CoinexResponse; +import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; +import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; +import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; + +@Path("") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public interface Coinex { + + @GET + @Path("v1/market/ticker/all") + CoinexResponse allMarketStatistics() + throws IOException, CoinexException; + + + @GET + @Path("v1/market/ticker") + CoinexResponse singleMarketStatistics(@QueryParam("market") String market) + throws IOException, CoinexException; + + + @GET + @Path("v2/spot/market") + CoinexResponse> marketStatus(@QueryParam("market") String markets) + throws IOException, CoinexException; + + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java index 8e174e83c31..e6571016f8e 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java @@ -1,31 +1,93 @@ package org.knowm.xchange.coinex; -import java.math.BigDecimal; -import java.util.ArrayList; +import java.time.Instant; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import lombok.experimental.UtilityClass; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; -import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.coinex.dto.marketdata.CoinexTickerV1; +import org.knowm.xchange.currency.CurrencyPair; import org.knowm.xchange.dto.account.Balance; import org.knowm.xchange.dto.account.Wallet; +import org.knowm.xchange.dto.marketdata.Ticker; +import org.knowm.xchange.dto.marketdata.Ticker.Builder; +import org.knowm.xchange.instrument.Instrument; +@UtilityClass public class CoinexAdapters { - public static Wallet adaptWallet(Map coinexBalances) { - List balances = new ArrayList<>(coinexBalances.size()); - for (Map.Entry balancePair : coinexBalances.entrySet()) { - Currency currency = new Currency(balancePair.getKey()); - BigDecimal total = - balancePair.getValue().getAvailable().add(balancePair.getValue().getFrozen()); - Balance balance = - new Balance( - currency, - total, - balancePair.getValue().getAvailable(), - balancePair.getValue().getFrozen()); - balances.add(balance); + private final Map SYMBOL_TO_CURRENCY_PAIR = new HashMap<>(); + + public Balance toBalance(CoinexBalanceInfo balance) { + return new Balance.Builder() + .currency(balance.getCurrency()) + .available(balance.getAvailable()) + .frozen(balance.getFrozen()) + .build(); + } + + public Wallet toWallet(List coinexBalanceInfos) { + List balances = coinexBalanceInfos.stream() + .map(CoinexAdapters::toBalance) + .collect(Collectors.toList()); + + return Wallet.Builder.from(balances).id("spot").build(); + } + + public String instrumentsToString(Collection instruments) { + if (instruments == null || instruments.isEmpty()) { + return null; + } else { + return instruments.stream().map(CoinexAdapters::toString).collect(Collectors.joining(",")); + } + } + + public void putSymbolMapping(String symbol, CurrencyPair currencyPair) { + SYMBOL_TO_CURRENCY_PAIR.put(symbol, currencyPair); + } + + public CurrencyPair toCurrencyPair(String symbol) { + return SYMBOL_TO_CURRENCY_PAIR.get(symbol); + } + + public String toString(Instrument instrument) { + if (instrument == null) { + return null; + } else { + return instrument.getBase().getCurrencyCode() + instrument.getCounter().getCurrencyCode(); } + } + + + public Ticker toTicker(String symbol, CoinexTickerV1 coinexTickerV1, Instant timestamp) { + return toTicker(toCurrencyPair(symbol), coinexTickerV1, timestamp); + } + + + public Ticker toTicker(Instrument instrument, CoinexTickerV1 coinexTickerV1, Instant timestamp) { + Builder builder = new Ticker.Builder(); + + if (instrument != null) { + builder.instrument(instrument); + } + + builder + .open(coinexTickerV1.getOpen24h()) + .last(coinexTickerV1.getVolume24h()) + .bid(coinexTickerV1.getBestBidPrice()) + .ask(coinexTickerV1.getBestAskPrice()) + .high(coinexTickerV1.getHigh24h()) + .low(coinexTickerV1.getLow24h()) + .volume(coinexTickerV1.getVolume24h()) + .timestamp(Date.from(timestamp)) + .bidSize(coinexTickerV1.getBestBidSize()) + .askSize(coinexTickerV1.getBestAskSize()) + .percentageChange(coinexTickerV1.get24hPercentageChange()); - return Wallet.Builder.from(balances).build(); + return builder.build(); } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java new file mode 100644 index 00000000000..0fcacfdfd20 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexErrorAdapter.java @@ -0,0 +1,24 @@ +package org.knowm.xchange.coinex; + +import lombok.experimental.UtilityClass; +import org.knowm.xchange.coinex.dto.CoinexException; +import org.knowm.xchange.exceptions.ExchangeException; +import org.knowm.xchange.exceptions.InstrumentNotValidException; + +@UtilityClass +public class CoinexErrorAdapter { + + public final int INVALID_MARKET_CODE = 3639; + + public ExchangeException adapt(CoinexException e) { + + switch (e.getCode()) { + case INVALID_MARKET_CODE: + return new InstrumentNotValidException(e.getMessage(), e); + + default: + } + + return new ExchangeException(e.getMessage(), e); + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java index 0497c3f0556..d48349e175a 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java @@ -1,25 +1,40 @@ package org.knowm.xchange.coinex; +import java.io.IOException; +import java.util.List; import org.knowm.xchange.BaseExchange; -import org.knowm.xchange.Exchange; import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; import org.knowm.xchange.coinex.service.CoinexAccountService; +import org.knowm.xchange.coinex.service.CoinexMarketDataService; +import org.knowm.xchange.coinex.service.CoinexMarketDataServiceRaw; +import org.knowm.xchange.currency.CurrencyPair; -public class CoinexExchange extends BaseExchange implements Exchange { +public class CoinexExchange extends BaseExchange { @Override protected void initServices() { - this.accountService = new CoinexAccountService(this); + accountService = new CoinexAccountService(this); + marketDataService = new CoinexMarketDataService(this); } @Override public ExchangeSpecification getDefaultExchangeSpecification() { - ExchangeSpecification exchangeSpecification = new ExchangeSpecification(this.getClass()); - exchangeSpecification.setSslUri("https://api.coinex.com"); - exchangeSpecification.setHost("www.coinex.com"); - exchangeSpecification.setPort(80); - exchangeSpecification.setExchangeName("Coinex"); - exchangeSpecification.setExchangeDescription("Bitstamp is a crypto-to-crypto exchange."); - return exchangeSpecification; + ExchangeSpecification specification = new ExchangeSpecification(this.getClass()); + specification.setSslUri("https://api.coinex.com"); + specification.setHost("www.coinex.com"); + specification.setExchangeName("Coinex"); + return specification; + } + + @Override + public void remoteInit() throws IOException { + CoinexMarketDataServiceRaw coinexMarketDataServiceRaw = (CoinexMarketDataServiceRaw) marketDataService; + + // initialize symbol mappings + List currencyPairInfos = coinexMarketDataServiceRaw.getCoinexCurrencyPairInfos(null); + currencyPairInfos.forEach(currencyPairInfo -> { + CoinexAdapters.putSymbolMapping(currencyPairInfo.getSymbol(), new CurrencyPair(currencyPairInfo.getBaseCurrency(), currencyPairInfo.getQuoteCurrency())); + }); } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyConverter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyConverter.java new file mode 100644 index 00000000000..5af8cdbabfa --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyConverter.java @@ -0,0 +1,13 @@ +package org.knowm.xchange.coinex.config.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.currency.Currency; + +/** Converts string value to {@code Currency} */ +public class StringToCurrencyConverter extends StdConverter { + + @Override + public Currency convert(String value) { + return Currency.getInstance(value); + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexException.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexException.java new file mode 100644 index 00000000000..53d38f4307b --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexException.java @@ -0,0 +1,18 @@ +package org.knowm.xchange.coinex.dto; + +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Value +@Builder +@Jacksonized +public class CoinexException extends RuntimeException { + + Integer code; + + Object data; + + String message; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java index feb8e00aa54..28ccebd1265 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java @@ -1,31 +1,22 @@ package org.knowm.xchange.coinex.dto; -import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import si.mazi.rescu.ExceptionalReturnContentException; +@Data public class CoinexResponse { - private final int code; - private final T data; - private final String message; + private Integer code; - public CoinexResponse( - @JsonProperty("code") int code, - @JsonProperty("data") T data, - @JsonProperty("message") String message) { - this.code = code; - this.data = data; - this.message = message; - } + private T data; - public int getCode() { - return code; - } + private String message; - public T getData() { - return data; + public void setCode(Integer code) { + if (code != 0) { + throw new ExceptionalReturnContentException(null); + } + this.code = code; } - public String getMessage() { - return message; - } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexAllMarketStatisticsV1.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexAllMarketStatisticsV1.java new file mode 100644 index 00000000000..4b94c7e8355 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexAllMarketStatisticsV1.java @@ -0,0 +1,21 @@ +package org.knowm.xchange.coinex.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.Map; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +@Data +@Builder +@Jacksonized +public class CoinexAllMarketStatisticsV1 { + + @JsonProperty("date") + private Instant timestamp; + + @JsonProperty("ticker") + private Map tickers; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java new file mode 100644 index 00000000000..8a37c04f748 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java @@ -0,0 +1,49 @@ +package org.knowm.xchange.coinex.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; +import org.knowm.xchange.coinex.config.converter.StringToCurrencyConverter; +import org.knowm.xchange.currency.Currency; + +@Data +@Builder +@Jacksonized +public class CoinexCurrencyPairInfo { + + @JsonProperty("base_ccy") + @JsonDeserialize(converter = StringToCurrencyConverter.class) + private Currency baseCurrency; + + @JsonProperty("quote_ccy") + @JsonDeserialize(converter = StringToCurrencyConverter.class) + private Currency quoteCurrency; + + @JsonProperty("base_ccy_precision") + private Integer baseCurrencyPrecision; + + @JsonProperty("quote_ccy_precision") + private Integer quoteCurrencyPrecision; + + @JsonProperty("is_amm_available") + private Boolean ammAvailable; + + @JsonProperty("is_margin_available") + private Boolean marginAvailable; + + @JsonProperty("maker_fee_rate") + private BigDecimal makerFeeRate; + + @JsonProperty("taker_fee_rate") + private BigDecimal takerFeeRate; + + @JsonProperty("market") + private String symbol; + + @JsonProperty("min_amount") + private BigDecimal minAssetAmount; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexSingleMarketStatisticsV1.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexSingleMarketStatisticsV1.java new file mode 100644 index 00000000000..1a4e21405e5 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexSingleMarketStatisticsV1.java @@ -0,0 +1,20 @@ +package org.knowm.xchange.coinex.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +@Data +@Builder +@Jacksonized +public class CoinexSingleMarketStatisticsV1 { + + @JsonProperty("date") + private Instant timestamp; + + @JsonProperty("ticker") + private CoinexTickerV1 ticker; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexTickerV1.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexTickerV1.java new file mode 100644 index 00000000000..6542a196565 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexTickerV1.java @@ -0,0 +1,48 @@ +package org.knowm.xchange.coinex.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import java.math.RoundingMode; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +@Data +@Builder +@Jacksonized +public class CoinexTickerV1 { + + @JsonProperty("vol") + private BigDecimal volume24h; + + @JsonProperty("low") + private BigDecimal low24h; + + @JsonProperty("open") + private BigDecimal open24h; + + @JsonProperty("high") + private BigDecimal high24h; + + @JsonProperty("last") + private BigDecimal last; + + @JsonProperty("buy") + private BigDecimal bestBidPrice; + + @JsonProperty("buy_amount") + private BigDecimal bestBidSize; + + @JsonProperty("sell") + private BigDecimal bestAskPrice; + + @JsonProperty("sell_amount") + private BigDecimal bestAskSize; + + public BigDecimal get24hPercentageChange() { + if (open24h != null && last != null && open24h.signum() > 0) { + return last.divide(open24h, 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + return null; + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java new file mode 100644 index 00000000000..dfdc792a465 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java @@ -0,0 +1,77 @@ +package org.knowm.xchange.coinex.service; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.coinex.CoinexErrorAdapter; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.CoinexException; +import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; +import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.Ticker; +import org.knowm.xchange.instrument.Instrument; +import org.knowm.xchange.service.marketdata.MarketDataService; +import org.knowm.xchange.service.marketdata.params.CurrencyPairsParam; +import org.knowm.xchange.service.marketdata.params.InstrumentsParams; +import org.knowm.xchange.service.marketdata.params.Params; + +public class CoinexMarketDataService extends CoinexMarketDataServiceRaw implements MarketDataService { + + + public CoinexMarketDataService(CoinexExchange exchange) { + super(exchange); + } + + @Override + public List getTickers(Params params) throws IOException { + // parse parameters + Collection instruments = Collections.emptyList(); + if (params instanceof InstrumentsParams) { + instruments = ((InstrumentsParams) params).getInstruments(); + } else if (params instanceof CurrencyPairsParam) { + instruments = ((CurrencyPairsParam) params).getCurrencyPairs().stream() + .map(Instrument.class::cast) + .collect(Collectors.toList()); + } + + try { + // get all + if (instruments.isEmpty()) { + CoinexAllMarketStatisticsV1 coinexAllMarketStatisticsV1 = getCoinexAllMarketStatisticsV1(); + return coinexAllMarketStatisticsV1.getTickers().entrySet().stream() + .map(entry -> CoinexAdapters.toTicker(entry.getKey(), entry.getValue(), coinexAllMarketStatisticsV1.getTimestamp())) + .filter(ticker -> ticker.getInstrument() != null) + .collect(Collectors.toList()); + } + else { + // get the first one + Instrument instrument = instruments.iterator().next(); + return Collections.singletonList(getTicker(instrument)); + } + + } catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } + } + + @Override + public Ticker getTicker(Instrument instrument, Object... args) throws IOException { + try { + CoinexSingleMarketStatisticsV1 singleMarketStatisticsV1 = getCoinexSingleMarketStatisticsV1(instrument); + return CoinexAdapters.toTicker(instrument, singleMarketStatisticsV1.getTicker(), singleMarketStatisticsV1.getTimestamp()); + + } catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } + } + + @Override + public Ticker getTicker(CurrencyPair currencyPair, Object... args) throws IOException { + return getTicker((Instrument) currencyPair, args); + } + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java new file mode 100644 index 00000000000..b6d6f3ea47f --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java @@ -0,0 +1,36 @@ +package org.knowm.xchange.coinex.service; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; +import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; +import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; +import org.knowm.xchange.instrument.Instrument; + +public class CoinexMarketDataServiceRaw extends CoinexBaseService { + + public CoinexMarketDataServiceRaw(CoinexExchange exchange) { + super(exchange); + } + + + public CoinexAllMarketStatisticsV1 getCoinexAllMarketStatisticsV1() throws IOException { + return coinex.allMarketStatistics().getData(); + } + + + public CoinexSingleMarketStatisticsV1 getCoinexSingleMarketStatisticsV1(Instrument instrument) throws IOException { + String market = CoinexAdapters.toString(instrument); + return coinex.singleMarketStatistics(market).getData(); + } + + + public List getCoinexCurrencyPairInfos(Collection instruments) throws IOException { + String marketsParam = CoinexAdapters.instrumentsToString(instruments); + return coinex.marketStatus(marketsParam).getData(); + } + +} diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceIntegration.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceIntegration.java new file mode 100644 index 00000000000..9b287664cda --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceIntegration.java @@ -0,0 +1,45 @@ +package org.knowm.xchange.coinex.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.Ticker; + +class CoinexMarketDataServiceIntegration { + + CoinexExchange exchange = ExchangeFactory.INSTANCE.createExchange(CoinexExchange.class); + + @Test + void valid_tickers() throws IOException { + List tickers = exchange.getMarketDataService().getTickers(null); + assertThat(tickers).isNotEmpty(); + + assertThat(tickers).allSatisfy(ticker -> { + assertThat(ticker.getInstrument()).isNotNull(); + assertThat(ticker.getLast()).isNotNull(); + if (ticker.getBid().signum() > 0 && ticker.getAsk().signum() > 0) { + assertThat(ticker.getBid()).isLessThan(ticker.getAsk()); + } + }); + } + + + @Test + void valid_single_ticker() throws IOException { + Ticker ticker = exchange.getMarketDataService().getTicker(CurrencyPair.BTC_USDT); + + assertThat(ticker.getInstrument()).isEqualTo(CurrencyPair.BTC_USDT); + assertThat(ticker.getLast()).isNotNull(); + + if (ticker.getBid().signum() > 0 && ticker.getAsk().signum() > 0) { + assertThat(ticker.getBid()).isLessThan(ticker.getAsk()); + } + + } + +} \ No newline at end of file From 1375bd84d78599bf2b5b3694a552f9798322cbdd Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Mon, 20 May 2024 23:29:41 +0200 Subject: [PATCH 06/13] [coinex] Add getting of balances --- .../xchange/coinex/CoinexAuthenticated.java | 26 ++++++------ .../coinex/dto/account/CoinexBalanceInfo.java | 32 +++++++------- .../coinex/service/CoinexAccountService.java | 18 ++++++-- .../service/CoinexAccountServiceRaw.java | 12 +++--- .../coinex/service/CoinexBaseService.java | 37 +++++++++------- .../coinex/service/CoinexV2Digest.java | 42 +++++++++++++++++++ .../coinex/service/CoinexV2DigestTest.java | 40 ++++++++++++++++++ 7 files changed, 150 insertions(+), 57 deletions(-) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java create mode 100644 xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java index b351e1f5735..602674f231c 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java @@ -5,27 +5,27 @@ import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.io.IOException; -import org.knowm.xchange.coinex.dto.account.CoinexBalances; +import java.util.List; +import org.knowm.xchange.coinex.dto.CoinexException; +import org.knowm.xchange.coinex.dto.CoinexResponse; +import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; import si.mazi.rescu.ParamsDigest; import si.mazi.rescu.SynchronizedValueFactory; -@Path("v1") +@Path("") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface CoinexAuthenticated { - String HEADER_AUTHORIZATION = "authorization"; - String HEADER_USER_AGENT = "User-Agent"; - @GET - @Path("balance/info") - CoinexBalances getWallet( - // @HeaderParam(HEADER_USER_AGENT) String user_agent_info, - @HeaderParam(HEADER_AUTHORIZATION) ParamsDigest sign, - @QueryParam("access_id") String access_id, - @QueryParam("tonce") SynchronizedValueFactory tonce) - throws IOException; + @Path("v2/assets/spot/balance") + CoinexResponse> balances( + @HeaderParam("X-COINEX-KEY") String apiKey, + @HeaderParam("X-COINEX-TIMESTAMP") SynchronizedValueFactory timestamp, + @HeaderParam("X-COINEX-SIGN") ParamsDigest signer) + throws IOException, CoinexException; + + } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexBalanceInfo.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexBalanceInfo.java index 59a6f979c33..2feb98870be 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexBalanceInfo.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/account/CoinexBalanceInfo.java @@ -1,29 +1,25 @@ package org.knowm.xchange.coinex.dto.account; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.math.BigDecimal; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; +import org.knowm.xchange.coinex.config.converter.StringToCurrencyConverter; +import org.knowm.xchange.currency.Currency; +@Data +@Builder +@Jacksonized public class CoinexBalanceInfo { - private final BigDecimal available; - private final BigDecimal frozen; + @JsonProperty("ccy") + @JsonDeserialize(converter = StringToCurrencyConverter.class) + private Currency currency; - public CoinexBalanceInfo( - @JsonProperty("available") BigDecimal available, @JsonProperty("frozen") BigDecimal frozen) { - this.available = available; - this.frozen = frozen; - } + private BigDecimal available; - public BigDecimal getAvailable() { - return available; - } + private BigDecimal frozen; - public BigDecimal getFrozen() { - return frozen; - } - - @Override - public String toString() { - return "CoinexBalanceInfo{" + "available=" + available + ", frozen=" + frozen + '}'; - } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountService.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountService.java index 5625d55d680..61622cc1542 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountService.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountService.java @@ -3,24 +3,36 @@ import java.io.IOException; import java.math.BigDecimal; import java.util.List; -import org.knowm.xchange.Exchange; import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.coinex.CoinexErrorAdapter; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.CoinexException; +import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.dto.account.AccountInfo; import org.knowm.xchange.dto.account.FundingRecord; +import org.knowm.xchange.dto.account.Wallet; import org.knowm.xchange.service.account.AccountService; import org.knowm.xchange.service.trade.params.TradeHistoryParams; import org.knowm.xchange.service.trade.params.WithdrawFundsParams; public class CoinexAccountService extends CoinexAccountServiceRaw implements AccountService { - public CoinexAccountService(Exchange exchange) { + public CoinexAccountService(CoinexExchange exchange) { super(exchange); } @Override public AccountInfo getAccountInfo() throws IOException { - return new AccountInfo(CoinexAdapters.adaptWallet(getCoinexBalances())); + try { + List spotBalances = getCoinexBalances(); + + Wallet wallet = CoinexAdapters.toWallet(spotBalances); + return new AccountInfo(wallet); + + } catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } } @Override diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountServiceRaw.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountServiceRaw.java index 731697d42d7..12f8cb05818 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountServiceRaw.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexAccountServiceRaw.java @@ -1,19 +1,17 @@ package org.knowm.xchange.coinex.service; import java.io.IOException; -import java.util.Map; -import org.knowm.xchange.Exchange; +import java.util.List; +import org.knowm.xchange.coinex.CoinexExchange; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; public class CoinexAccountServiceRaw extends CoinexBaseService { - protected CoinexAccountServiceRaw(Exchange exchange) { + public CoinexAccountServiceRaw(CoinexExchange exchange) { super(exchange); } - public Map getCoinexBalances() throws IOException { - // return - // coinex.getWallet(signatureCreator,apiKey,exchange.getNonceFactory()).getBalances(); - return null; + public List getCoinexBalances() throws IOException { + return coinexAuthenticated.balances(apiKey, exchange.getNonceFactory(), coinexV2ParamsDigest).getData(); } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexBaseService.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexBaseService.java index 04a6b5fc209..40a785d2369 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexBaseService.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexBaseService.java @@ -1,29 +1,34 @@ package org.knowm.xchange.coinex.service; -import org.knowm.xchange.Exchange; import org.knowm.xchange.client.ExchangeRestProxyBuilder; +import org.knowm.xchange.coinex.Coinex; import org.knowm.xchange.coinex.CoinexAuthenticated; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.config.CoinexJacksonObjectMapperFactory; import org.knowm.xchange.service.BaseExchangeService; import org.knowm.xchange.service.BaseService; -public class CoinexBaseService extends BaseExchangeService implements BaseService { +public class CoinexBaseService extends BaseExchangeService implements BaseService { protected final String apiKey; - protected final CoinexAuthenticated coinex; - protected final String USER_AGENT_INFO = - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36"; + protected final Coinex coinex; + protected final CoinexAuthenticated coinexAuthenticated; + protected final CoinexV2Digest coinexV2ParamsDigest; - /** - * Constructor - * - * @param exchange - */ - protected CoinexBaseService(Exchange exchange) { + public CoinexBaseService(CoinexExchange exchange) { super(exchange); - this.coinex = - ExchangeRestProxyBuilder.forInterface( - CoinexAuthenticated.class, exchange.getExchangeSpecification()) - .build(); - this.apiKey = exchange.getExchangeSpecification().getApiKey(); + coinex = ExchangeRestProxyBuilder + .forInterface(Coinex.class, exchange.getExchangeSpecification()) + .clientConfigCustomizer(clientConfig -> clientConfig.setJacksonObjectMapperFactory(new CoinexJacksonObjectMapperFactory())) + .build(); + coinexAuthenticated = ExchangeRestProxyBuilder + .forInterface(CoinexAuthenticated.class, exchange.getExchangeSpecification()) + .clientConfigCustomizer(clientConfig -> clientConfig.setJacksonObjectMapperFactory(new CoinexJacksonObjectMapperFactory())) + .build(); + apiKey = exchange.getExchangeSpecification().getApiKey(); + + coinexV2ParamsDigest = + CoinexV2Digest.createInstance(exchange.getExchangeSpecification().getSecretKey()); + } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java new file mode 100644 index 00000000000..49f4c5b72fb --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexV2Digest.java @@ -0,0 +1,42 @@ +package org.knowm.xchange.coinex.service; + +import java.nio.charset.StandardCharsets; +import javax.crypto.Mac; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.knowm.xchange.service.BaseParamsDigest; +import org.knowm.xchange.utils.DigestUtils; +import si.mazi.rescu.RestInvocation; + +public final class CoinexV2Digest extends BaseParamsDigest { + + private CoinexV2Digest(String secretKeyBase64) { + super(secretKeyBase64, HMAC_SHA_256); + } + + + public static CoinexV2Digest createInstance(String secretKeyBase64) { + return secretKeyBase64 == null ? null : new CoinexV2Digest(secretKeyBase64); + } + + + @SneakyThrows + @Override + public String digestParams(RestInvocation restInvocation) { + String method = restInvocation.getHttpMethod(); + String path = restInvocation.getPath(); + + String query = StringUtils.defaultIfEmpty(restInvocation.getQueryString(), ""); + String body = StringUtils.defaultIfEmpty(restInvocation.getRequestBody(), ""); + + String timestamp = restInvocation.getHttpHeadersFromParams().get("X-COINEX-TIMESTAMP"); + + String payloadToSign = + String.format("%s/%s%s%s%s", method, path, query, body, timestamp); + + Mac mac = getMac(); + mac.update(payloadToSign.getBytes(StandardCharsets.UTF_8)); + + return DigestUtils.bytesToHex(mac.doFinal()); + } +} diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java new file mode 100644 index 00000000000..363e97280a7 --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java @@ -0,0 +1,40 @@ +package org.knowm.xchange.coinex.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import si.mazi.rescu.RestInvocation; + +@ExtendWith(MockitoExtension.class) +class CoinexV2DigestTest { + + @Mock + RestInvocation restInvocation; + + @Test + void signature() { + CoinexV2Digest coinexV2Digest = CoinexV2Digest.createInstance("44F7E70A2952C1006E0446044F7757AA60D2F9246D269356"); + + when(restInvocation.getHttpMethod()).thenReturn("GET"); + when(restInvocation.getPath()).thenReturn("v2/assets/spot/balance"); + when(restInvocation.getQueryString()).thenReturn(""); + when(restInvocation.getRequestBody()).thenReturn(null); + Map headers = new HashMap<>(); + headers.put("X-COINEX-TIMESTAMP", "1714992192553"); + when(restInvocation.getHttpHeadersFromParams()).thenReturn(headers); + + String actual = coinexV2Digest.digestParams(restInvocation); + String expected = + "1e1dd41be7988975295931013c05505a5ed67a591d8e625d10ed137b3fe9592a"; + + assertThat(actual).isEqualTo(expected); + } + + +} \ No newline at end of file From 4ebb9bd0343b799852c68c752d3722b5e723a536 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Tue, 21 May 2024 00:16:18 +0200 Subject: [PATCH 07/13] [coinex] Add integration test for AccountService --- xchange-coinex/.gitignore | 3 +- xchange-coinex/README.md | 12 ++++++- .../example.integration-test.env.properties | 2 ++ xchange-coinex/pom.xml | 13 +++++++ .../CoinexAccountServiceIntegration.java | 35 +++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 xchange-coinex/example.integration-test.env.properties create mode 100644 xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexAccountServiceIntegration.java diff --git a/xchange-coinex/.gitignore b/xchange-coinex/.gitignore index 42c7d2cd0eb..b1bb6c087ff 100644 --- a/xchange-coinex/.gitignore +++ b/xchange-coinex/.gitignore @@ -1 +1,2 @@ -http-client.private.env.json \ No newline at end of file +http-client.private.env.json +integration-test.env.properties \ No newline at end of file diff --git a/xchange-coinex/README.md b/xchange-coinex/README.md index 6f4d4742fa7..f728ba8eda6 100644 --- a/xchange-coinex/README.md +++ b/xchange-coinex/README.md @@ -8,4 +8,14 @@ Some requests need authorization, so the api credentials have to be stored in `h > Never commit your api credentials to the repository! -[HTTP Client documentation](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) \ No newline at end of file +[HTTP Client documentation](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) + +## Running integration tests that require API keys + +Integration tests that require API keys read them from environment variables. They can be defined in `integration-test.env.properties`. Sample content can be found in `example.integration-test.env.properties`. + +If no keys are provided the integration tests that need them are skipped. + +> [!CAUTION] +> Never commit your api credentials to the repository! + diff --git a/xchange-coinex/example.integration-test.env.properties b/xchange-coinex/example.integration-test.env.properties new file mode 100644 index 00000000000..d8421d99d6c --- /dev/null +++ b/xchange-coinex/example.integration-test.env.properties @@ -0,0 +1,2 @@ +apiKey=change_me +secretKey=change_me diff --git a/xchange-coinex/pom.xml b/xchange-coinex/pom.xml index fcdb4e55bc8..434b6a495f8 100644 --- a/xchange-coinex/pom.xml +++ b/xchange-coinex/pom.xml @@ -54,6 +54,19 @@ + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + integration-test.env.properties + + + + + com.github.ekryd.sortpom diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexAccountServiceIntegration.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexAccountServiceIntegration.java new file mode 100644 index 00000000000..353286f1d46 --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexAccountServiceIntegration.java @@ -0,0 +1,35 @@ +package org.knowm.xchange.coinex.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + +import java.io.IOException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.dto.account.AccountInfo; + +class CoinexAccountServiceIntegration { + + static CoinexExchange exchange; + + @BeforeAll + public static void credentialsPresent() { + // skip if there are no credentials + String apiKey = System.getProperty("apiKey"); + String secretKey = System.getProperty("secretKey"); + assumeThat(apiKey).isNotEmpty(); + assumeThat(secretKey).isNotEmpty(); + + exchange = ExchangeFactory.INSTANCE.createExchange(CoinexExchange.class, apiKey, secretKey); + } + + @Test + void valid_balances() throws IOException { + AccountInfo accountInfo = exchange.getAccountService().getAccountInfo(); + assertThat(accountInfo.getWallet("spot").getBalances()).isNotEmpty(); + } + + +} \ No newline at end of file From a8ab7ec06e048b237bb061d93446ec7cdd9da961 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Tue, 21 May 2024 23:36:29 +0200 Subject: [PATCH 08/13] [coinex] Add getting of chain infos --- .../java/org/knowm/xchange/coinex/Coinex.java | 8 ++++ .../dto/marketdata/CoinexChainInfo.java | 46 +++++++++++++++++++ .../service/CoinexMarketDataServiceRaw.java | 7 +++ ...CoinexMarketDataServiceRawIntegration.java | 28 +++++++++++ 4 files changed, 89 insertions(+) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexChainInfo.java create mode 100644 xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRawIntegration.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java index 69737244d19..c331ad96959 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java @@ -8,9 +8,11 @@ import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.util.List; +import java.util.Map; import org.knowm.xchange.coinex.dto.CoinexException; import org.knowm.xchange.coinex.dto.CoinexResponse; import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; +import org.knowm.xchange.coinex.dto.marketdata.CoinexChainInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; @@ -19,6 +21,12 @@ @Consumes(MediaType.APPLICATION_JSON) public interface Coinex { + @GET + @Path("v1/common/asset/config") + CoinexResponse> allChainInfos() + throws IOException, CoinexException; + + @GET @Path("v1/market/ticker/all") CoinexResponse allMarketStatistics() diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexChainInfo.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexChainInfo.java new file mode 100644 index 00000000000..7a5757e9a5f --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexChainInfo.java @@ -0,0 +1,46 @@ +package org.knowm.xchange.coinex.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.math.BigDecimal; +import java.net.URI; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; +import org.knowm.xchange.coinex.config.converter.StringToCurrencyConverter; +import org.knowm.xchange.currency.Currency; + +@Data +@Builder +@Jacksonized +public class CoinexChainInfo { + + @JsonProperty("asset") + @JsonDeserialize(converter = StringToCurrencyConverter.class) + private Currency currency; + + @JsonProperty("chain") + private String chainName; + + @JsonProperty("withdrawal_precision") + private Integer withdrawalPrecision; + + @JsonProperty("can_deposit") + private Boolean depositEnabled; + + @JsonProperty("can_withdraw") + private Boolean withdrawEnabled; + + @JsonProperty("deposit_least_amount") + private BigDecimal minDepositAmount; + + @JsonProperty("withdraw_least_amount") + private BigDecimal minWitdrawAmount; + + @JsonProperty("withdraw_tx_fee") + private BigDecimal witdrawFeeAmount; + + @JsonProperty("explorer_asset_url") + private URI explorerAssetUrl; + +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java index b6d6f3ea47f..d5207545e12 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java @@ -1,11 +1,13 @@ package org.knowm.xchange.coinex.service; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.knowm.xchange.coinex.CoinexAdapters; import org.knowm.xchange.coinex.CoinexExchange; import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; +import org.knowm.xchange.coinex.dto.marketdata.CoinexChainInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; import org.knowm.xchange.instrument.Instrument; @@ -17,6 +19,11 @@ public CoinexMarketDataServiceRaw(CoinexExchange exchange) { } + public List getAllCoinexChainInfos() throws IOException { + return new ArrayList<>(coinex.allChainInfos().getData().values()); + } + + public CoinexAllMarketStatisticsV1 getCoinexAllMarketStatisticsV1() throws IOException { return coinex.allMarketStatistics().getData(); } diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRawIntegration.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRawIntegration.java new file mode 100644 index 00000000000..b5283174d7a --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRawIntegration.java @@ -0,0 +1,28 @@ +package org.knowm.xchange.coinex.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.coinex.CoinexExchange; +import org.knowm.xchange.coinex.dto.marketdata.CoinexChainInfo; + +class CoinexMarketDataServiceRawIntegration { + + CoinexExchange exchange = ExchangeFactory.INSTANCE.createExchange(CoinexExchange.class); + + @Test + void valid_chainInfos() throws IOException { + CoinexMarketDataServiceRaw coinexMarketDataServiceRaw = (CoinexMarketDataServiceRaw) exchange.getMarketDataService(); + List chainInfos = coinexMarketDataServiceRaw.getAllCoinexChainInfos(); + + assertThat(chainInfos).allSatisfy(chainInfo -> { + assertThat(chainInfo.getCurrency()).isNotNull(); + assertThat(chainInfo.getChainName()).isNotEmpty(); + }); + } + + +} \ No newline at end of file From ca24d475a79916518c09a3cbee94088117c20358 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Fri, 24 May 2024 16:54:33 +0200 Subject: [PATCH 09/13] [coinex] Add getting of orderbook --- .../java/org/knowm/xchange/coinex/Coinex.java | 8 ++ .../knowm/xchange/coinex/CoinexAdapters.java | 81 ++++++++++++++----- .../StringToCurrencyPairConverter.java | 14 ++++ .../dto/marketdata/CoinexMarketDepth.java | 58 +++++++++++++ .../service/CoinexMarketDataService.java | 59 +++++++++++--- .../service/CoinexMarketDataServiceRaw.java | 12 +++ .../service/params/CoinexOrderBookParams.java | 21 +++++ .../CoinexMarketDataServiceIntegration.java | 24 ++++++ .../src/test/resources/rest/spot.v2.http | 2 +- 9 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyPairConverter.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexMarketDepth.java create mode 100644 xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/params/CoinexOrderBookParams.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java index c331ad96959..998a4ba3485 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java @@ -14,6 +14,7 @@ import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; import org.knowm.xchange.coinex.dto.marketdata.CoinexChainInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; +import org.knowm.xchange.coinex.dto.marketdata.CoinexMarketDepth; import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; @Path("") @@ -45,4 +46,11 @@ CoinexResponse> marketStatus(@QueryParam("market") throws IOException, CoinexException; + @GET + @Path("v2/spot/depth") + CoinexResponse marketDepth(@QueryParam("market") String market, + @QueryParam("limit") Integer limit, @QueryParam("interval") Integer interval) + throws IOException, CoinexException; + + } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java index e6571016f8e..6a6efda5f9f 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java @@ -9,12 +9,16 @@ import java.util.stream.Collectors; import lombok.experimental.UtilityClass; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; +import org.knowm.xchange.coinex.dto.marketdata.CoinexMarketDepth; import org.knowm.xchange.coinex.dto.marketdata.CoinexTickerV1; import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order.OrderType; import org.knowm.xchange.dto.account.Balance; import org.knowm.xchange.dto.account.Wallet; +import org.knowm.xchange.dto.marketdata.OrderBook; import org.knowm.xchange.dto.marketdata.Ticker; import org.knowm.xchange.dto.marketdata.Ticker.Builder; +import org.knowm.xchange.dto.trade.LimitOrder; import org.knowm.xchange.instrument.Instrument; @UtilityClass @@ -22,21 +26,6 @@ public class CoinexAdapters { private final Map SYMBOL_TO_CURRENCY_PAIR = new HashMap<>(); - public Balance toBalance(CoinexBalanceInfo balance) { - return new Balance.Builder() - .currency(balance.getCurrency()) - .available(balance.getAvailable()) - .frozen(balance.getFrozen()) - .build(); - } - - public Wallet toWallet(List coinexBalanceInfos) { - List balances = coinexBalanceInfos.stream() - .map(CoinexAdapters::toBalance) - .collect(Collectors.toList()); - - return Wallet.Builder.from(balances).id("spot").build(); - } public String instrumentsToString(Collection instruments) { if (instruments == null || instruments.isEmpty()) { @@ -46,14 +35,55 @@ public String instrumentsToString(Collection instruments) { } } + public void putSymbolMapping(String symbol, CurrencyPair currencyPair) { SYMBOL_TO_CURRENCY_PAIR.put(symbol, currencyPair); } + + public Balance toBalance(CoinexBalanceInfo balance) { + return new Balance.Builder() + .currency(balance.getCurrency()) + .available(balance.getAvailable()) + .frozen(balance.getFrozen()) + .build(); + } + + public CurrencyPair toCurrencyPair(String symbol) { return SYMBOL_TO_CURRENCY_PAIR.get(symbol); } + + public OrderBook toOrderBook(CoinexMarketDepth coinexMarketDepth) { + List asks = coinexMarketDepth.getDepth().getAsks().stream() + .map(priceSizeEntry -> + new LimitOrder( + OrderType.ASK, + priceSizeEntry.getSize(), + coinexMarketDepth.getCurrencyPair(), + null, + null, + priceSizeEntry.getPrice()) + ) + .collect(Collectors.toList()); + + List bids = coinexMarketDepth.getDepth().getBids().stream() + .map(priceSizeEntry -> + new LimitOrder( + OrderType.BID, + priceSizeEntry.getSize(), + coinexMarketDepth.getCurrencyPair(), + null, + null, + priceSizeEntry.getPrice()) + ) + .collect(Collectors.toList()); + + return new OrderBook(Date.from(coinexMarketDepth.getDepth().getUpdatedAt()), asks, bids); + } + + public String toString(Instrument instrument) { if (instrument == null) { return null; @@ -63,11 +93,6 @@ public String toString(Instrument instrument) { } - public Ticker toTicker(String symbol, CoinexTickerV1 coinexTickerV1, Instant timestamp) { - return toTicker(toCurrencyPair(symbol), coinexTickerV1, timestamp); - } - - public Ticker toTicker(Instrument instrument, CoinexTickerV1 coinexTickerV1, Instant timestamp) { Builder builder = new Ticker.Builder(); @@ -90,4 +115,20 @@ public Ticker toTicker(Instrument instrument, CoinexTickerV1 coinexTickerV1, Ins return builder.build(); } + + + public Ticker toTicker(String symbol, CoinexTickerV1 coinexTickerV1, Instant timestamp) { + return toTicker(toCurrencyPair(symbol), coinexTickerV1, timestamp); + } + + + public Wallet toWallet(List coinexBalanceInfos) { + List balances = coinexBalanceInfos.stream() + .map(CoinexAdapters::toBalance) + .collect(Collectors.toList()); + + return Wallet.Builder.from(balances).id("spot").build(); + } + + } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyPairConverter.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyPairConverter.java new file mode 100644 index 00000000000..8f3d1342bec --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/config/converter/StringToCurrencyPairConverter.java @@ -0,0 +1,14 @@ +package org.knowm.xchange.coinex.config.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.knowm.xchange.coinex.CoinexAdapters; +import org.knowm.xchange.currency.CurrencyPair; + +/** Converts string to {@code CurrencyPair} */ +public class StringToCurrencyPairConverter extends StdConverter { + + @Override + public CurrencyPair convert(String value) { + return CoinexAdapters.toCurrencyPair(value); + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexMarketDepth.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexMarketDepth.java new file mode 100644 index 00000000000..eb9846bdab2 --- /dev/null +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexMarketDepth.java @@ -0,0 +1,58 @@ +package org.knowm.xchange.coinex.dto.marketdata; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; +import org.knowm.xchange.coinex.config.converter.StringToCurrencyPairConverter; +import org.knowm.xchange.currency.CurrencyPair; + +@Data +@Builder +@Jacksonized +public class CoinexMarketDepth { + + @JsonProperty("depth") + Depth depth; + + @JsonProperty("is_full") + Boolean isFull; + + @JsonProperty("market") + @JsonDeserialize(converter = StringToCurrencyPairConverter.class) + CurrencyPair currencyPair; + + @Data + @Builder + @Jacksonized + public static class Depth { + + @JsonProperty("asks") + List asks; + + @JsonProperty("bids") + List bids; + + @JsonProperty("checksum") + Long checksum; + + @JsonProperty("updated_at") + Instant updatedAt; + + } + + @Data + @Builder + @Jacksonized + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public static class PriceSizeEntry { + BigDecimal price; + + BigDecimal size; + } +} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java index dfdc792a465..30e162eb1ba 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataService.java @@ -5,13 +5,17 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.Validate; import org.knowm.xchange.coinex.CoinexAdapters; import org.knowm.xchange.coinex.CoinexErrorAdapter; import org.knowm.xchange.coinex.CoinexExchange; import org.knowm.xchange.coinex.dto.CoinexException; import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; +import org.knowm.xchange.coinex.service.params.CoinexOrderBookParams; import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.OrderBook; import org.knowm.xchange.dto.marketdata.Ticker; import org.knowm.xchange.instrument.Instrument; import org.knowm.xchange.service.marketdata.MarketDataService; @@ -26,6 +30,25 @@ public CoinexMarketDataService(CoinexExchange exchange) { super(exchange); } + + @Override + public Ticker getTicker(CurrencyPair currencyPair, Object... args) throws IOException { + return getTicker((Instrument) currencyPair, args); + } + + + @Override + public Ticker getTicker(Instrument instrument, Object... args) throws IOException { + try { + CoinexSingleMarketStatisticsV1 singleMarketStatisticsV1 = getCoinexSingleMarketStatisticsV1(instrument); + return CoinexAdapters.toTicker(instrument, singleMarketStatisticsV1.getTicker(), singleMarketStatisticsV1.getTimestamp()); + + } catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } + } + + @Override public List getTickers(Params params) throws IOException { // parse parameters @@ -58,20 +81,36 @@ public List getTickers(Params params) throws IOException { } } - @Override - public Ticker getTicker(Instrument instrument, Object... args) throws IOException { - try { - CoinexSingleMarketStatisticsV1 singleMarketStatisticsV1 = getCoinexSingleMarketStatisticsV1(instrument); - return CoinexAdapters.toTicker(instrument, singleMarketStatisticsV1.getTicker(), singleMarketStatisticsV1.getTimestamp()); - } catch (CoinexException e) { - throw CoinexErrorAdapter.adapt(e); - } + @Override + public OrderBook getOrderBook(CurrencyPair currencyPair, Object... args) throws IOException { + return getOrderBook((Instrument) currencyPair, args); } + @Override - public Ticker getTicker(CurrencyPair currencyPair, Object... args) throws IOException { - return getTicker((Instrument) currencyPair, args); + public OrderBook getOrderBook(Instrument instrument, Object... args) throws IOException { + Integer limit = (Integer) ArrayUtils.get(args, 0); + Integer interval = (Integer) ArrayUtils.get(args, 1); + + return getOrderBook(CoinexOrderBookParams.builder() + .instrument(instrument) + .limit(limit) + .interval(interval) + .build() + ); } + + @Override + public OrderBook getOrderBook(Params params) throws IOException { + Validate.isInstanceOf(CoinexOrderBookParams.class, params); + CoinexOrderBookParams coinexOrderBookParams = (CoinexOrderBookParams) params; + + try { + return CoinexAdapters.toOrderBook(getCoinexOrderBook(coinexOrderBookParams)); + } catch (CoinexException e) { + throw CoinexErrorAdapter.adapt(e); + } + } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java index d5207545e12..80e0c48464d 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/service/CoinexMarketDataServiceRaw.java @@ -4,12 +4,15 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; import org.knowm.xchange.coinex.CoinexAdapters; import org.knowm.xchange.coinex.CoinexExchange; import org.knowm.xchange.coinex.dto.marketdata.CoinexAllMarketStatisticsV1; import org.knowm.xchange.coinex.dto.marketdata.CoinexChainInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; +import org.knowm.xchange.coinex.dto.marketdata.CoinexMarketDepth; import org.knowm.xchange.coinex.dto.marketdata.CoinexSingleMarketStatisticsV1; +import org.knowm.xchange.coinex.service.params.CoinexOrderBookParams; import org.knowm.xchange.instrument.Instrument; public class CoinexMarketDataServiceRaw extends CoinexBaseService { @@ -40,4 +43,13 @@ public List getCoinexCurrencyPairInfos(Collection { + assertThat(limitOrder.getInstrument()).isEqualTo(CurrencyPair.BTC_USDT); + assertThat(limitOrder.getType()).isEqualTo(OrderType.BID); + }); + + assertThat(orderBook.getAsks()).allSatisfy(limitOrder -> { + assertThat(limitOrder.getInstrument()).isEqualTo(CurrencyPair.BTC_USDT); + assertThat(limitOrder.getType()).isEqualTo(OrderType.ASK); + }); + + } + } \ No newline at end of file diff --git a/xchange-coinex/src/test/resources/rest/spot.v2.http b/xchange-coinex/src/test/resources/rest/spot.v2.http index c70cc577d8d..6c90f75eee7 100644 --- a/xchange-coinex/src/test/resources/rest/spot.v2.http +++ b/xchange-coinex/src/test/resources/rest/spot.v2.http @@ -11,6 +11,6 @@ GET {{api_host}}/v2/spot/index ### Get Market Depth -GET {{api_host}}/v2/spot/depth +GET {{api_host}}/v2/spot/depth?market=BTCUSDT&limit=50&interval=0 From e2b73a09b702afdebc9dae4f3aaa03571eb50963 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 25 May 2024 01:47:48 +0200 Subject: [PATCH 10/13] [coinex] Configure logging in tests --- xchange-coinex/src/test/resources/logback.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 xchange-coinex/src/test/resources/logback.xml diff --git a/xchange-coinex/src/test/resources/logback.xml b/xchange-coinex/src/test/resources/logback.xml new file mode 100644 index 00000000000..c823ac40a99 --- /dev/null +++ b/xchange-coinex/src/test/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + + + + %d{HH:mm:ss.SSS} [%contextName] [%thread] %-5level %logger{36} - %msg %xEx%n + + + + + + + + + + + + + + From 4571547c3a76605d4f43cd3164a5eaeddb2f6573 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 25 May 2024 01:48:29 +0200 Subject: [PATCH 11/13] [coinex] Disable body annotation for GET methods --- .../src/main/java/org/knowm/xchange/coinex/Coinex.java | 2 -- .../main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java index 998a4ba3485..64004f2e363 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/Coinex.java @@ -1,6 +1,5 @@ package org.knowm.xchange.coinex; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -19,7 +18,6 @@ @Path("") @Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) public interface Coinex { @GET diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java index 602674f231c..f69e6221c90 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAuthenticated.java @@ -1,6 +1,5 @@ package org.knowm.xchange.coinex; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; @@ -16,7 +15,6 @@ @Path("") @Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) public interface CoinexAuthenticated { @GET From e27a06345ab683d25750db81ac1a98fce42475cb Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 25 May 2024 01:49:12 +0200 Subject: [PATCH 12/13] [coinex] Add instrument metadata initialization --- .../knowm/xchange/coinex/CoinexAdapters.java | 12 +++++++++ .../knowm/xchange/coinex/CoinexExchange.java | 15 +++++++++++ .../marketdata/CoinexCurrencyPairInfo.java | 6 +++++ .../coinex/CoinexExchangeIntegration.java | 25 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeIntegration.java diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java index 6a6efda5f9f..937bcb1dcea 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexAdapters.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import lombok.experimental.UtilityClass; import org.knowm.xchange.coinex.dto.account.CoinexBalanceInfo; +import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; import org.knowm.xchange.coinex.dto.marketdata.CoinexMarketDepth; import org.knowm.xchange.coinex.dto.marketdata.CoinexTickerV1; import org.knowm.xchange.currency.CurrencyPair; @@ -18,6 +19,7 @@ import org.knowm.xchange.dto.marketdata.OrderBook; import org.knowm.xchange.dto.marketdata.Ticker; import org.knowm.xchange.dto.marketdata.Ticker.Builder; +import org.knowm.xchange.dto.meta.InstrumentMetaData; import org.knowm.xchange.dto.trade.LimitOrder; import org.knowm.xchange.instrument.Instrument; @@ -55,6 +57,16 @@ public CurrencyPair toCurrencyPair(String symbol) { } + public InstrumentMetaData toInstrumentMetaData(CoinexCurrencyPairInfo coinexCurrencyPairInfo) { + return new InstrumentMetaData.Builder() + .tradingFee(coinexCurrencyPairInfo.getTakerFeeRate()) + .minimumAmount(coinexCurrencyPairInfo.getMinAssetAmount()) + .volumeScale(coinexCurrencyPairInfo.getBaseCurrencyPrecision()) + .priceScale(coinexCurrencyPairInfo.getQuoteCurrencyPrecision()) + .build(); + } + + public OrderBook toOrderBook(CoinexMarketDepth coinexMarketDepth) { List asks = coinexMarketDepth.getDepth().getAsks().stream() .map(priceSizeEntry -> diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java index d48349e175a..03c31363b18 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/CoinexExchange.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.knowm.xchange.BaseExchange; import org.knowm.xchange.ExchangeSpecification; import org.knowm.xchange.coinex.dto.marketdata.CoinexCurrencyPairInfo; @@ -9,6 +11,9 @@ import org.knowm.xchange.coinex.service.CoinexMarketDataService; import org.knowm.xchange.coinex.service.CoinexMarketDataServiceRaw; import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.meta.ExchangeMetaData; +import org.knowm.xchange.dto.meta.InstrumentMetaData; +import org.knowm.xchange.instrument.Instrument; public class CoinexExchange extends BaseExchange { @@ -36,5 +41,15 @@ public void remoteInit() throws IOException { currencyPairInfos.forEach(currencyPairInfo -> { CoinexAdapters.putSymbolMapping(currencyPairInfo.getSymbol(), new CurrencyPair(currencyPairInfo.getBaseCurrency(), currencyPairInfo.getQuoteCurrency())); }); + + // initialize instrument metadata + Map instruments = currencyPairInfos.stream() + .collect(Collectors.toMap( + CoinexCurrencyPairInfo::getCurrencyPair, + CoinexAdapters::toInstrumentMetaData) + ); + + exchangeMetaData = new ExchangeMetaData(instruments, null, null, null, null); + } } diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java index 8a37c04f748..68ff3c7abb4 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/marketdata/CoinexCurrencyPairInfo.java @@ -8,6 +8,7 @@ import lombok.extern.jackson.Jacksonized; import org.knowm.xchange.coinex.config.converter.StringToCurrencyConverter; import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.currency.CurrencyPair; @Data @Builder @@ -46,4 +47,9 @@ public class CoinexCurrencyPairInfo { @JsonProperty("min_amount") private BigDecimal minAssetAmount; + + public CurrencyPair getCurrencyPair() { + return new CurrencyPair(baseCurrency, quoteCurrency); + } + } diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeIntegration.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeIntegration.java new file mode 100644 index 00000000000..416159a01f9 --- /dev/null +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/CoinexExchangeIntegration.java @@ -0,0 +1,25 @@ +package org.knowm.xchange.coinex; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.meta.InstrumentMetaData; +import org.knowm.xchange.instrument.Instrument; + +class CoinexExchangeIntegration { + + CoinexExchange exchange = ExchangeFactory.INSTANCE.createExchange(CoinexExchange.class); + + + @Test + void valid_metadata() { + assertThat(exchange.getExchangeMetaData()).isNotNull(); + Map instruments = exchange.getExchangeMetaData().getInstruments(); + assertThat(instruments).containsKey(CurrencyPair.BTC_USDT); + } + + +} \ No newline at end of file From 1395a5d6b022eaacec5e8e580aaea18b33141879 Mon Sep 17 00:00:00 2001 From: Dmitri Karpovich Date: Sat, 25 May 2024 14:46:04 +0200 Subject: [PATCH 13/13] [coinex] Minor fixes --- pom.xml | 1 - .../java/org/knowm/xchange/coinex/dto/CoinexResponse.java | 2 +- .../org/knowm/xchange/coinex/service/CoinexV2DigestTest.java | 5 ++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 90dad44ce7c..e36d6104943 100644 --- a/pom.xml +++ b/pom.xml @@ -394,7 +394,6 @@ org.slf4j slf4j-api - ${version.slf4j} diff --git a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java index 28ccebd1265..de2698154b3 100644 --- a/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java +++ b/xchange-coinex/src/main/java/org/knowm/xchange/coinex/dto/CoinexResponse.java @@ -14,7 +14,7 @@ public class CoinexResponse { public void setCode(Integer code) { if (code != 0) { - throw new ExceptionalReturnContentException(null); + throw new ExceptionalReturnContentException(String.valueOf(code)); } this.code = code; } diff --git a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java index 363e97280a7..fe7d6255f75 100644 --- a/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java +++ b/xchange-coinex/src/test/java/org/knowm/xchange/coinex/service/CoinexV2DigestTest.java @@ -19,7 +19,7 @@ class CoinexV2DigestTest { @Test void signature() { - CoinexV2Digest coinexV2Digest = CoinexV2Digest.createInstance("44F7E70A2952C1006E0446044F7757AA60D2F9246D269356"); + CoinexV2Digest coinexV2Digest = CoinexV2Digest.createInstance("a"); when(restInvocation.getHttpMethod()).thenReturn("GET"); when(restInvocation.getPath()).thenReturn("v2/assets/spot/balance"); @@ -30,8 +30,7 @@ void signature() { when(restInvocation.getHttpHeadersFromParams()).thenReturn(headers); String actual = coinexV2Digest.digestParams(restInvocation); - String expected = - "1e1dd41be7988975295931013c05505a5ed67a591d8e625d10ed137b3fe9592a"; + String expected = "3d47a904753df7d52fa6c37213bff7db8363249f8f0fed22bf41137805b57a56"; assertThat(actual).isEqualTo(expected); }