diff --git a/pom.xml b/pom.xml index 821b951ff37..fd0a22a1835 100644 --- a/pom.xml +++ b/pom.xml @@ -1,585 +1,586 @@ - - - 4.0.0 - - - 3.0 - - - org.knowm.xchange - xchange-parent - 5.1.1-SNAPSHOT - pom - - XChange - XChange is a Java library providing a simple and consistent API for interacting with - a diverse set of cryptocurrency exchanges. - - - http://knowm.org/open-source/xchange - 2012 - - - - Knowm Inc. - http://knowm.org/open-source/xchange/ - - - - UTF-8 - UTF-8 - - 1.8 - 3.23.1 - 3.12.0 - 2.14.1 - 2.1.0 - 1.7.0 - 4.13.2 - 1.18.22 - 3.8.2 - 1.4.5 - 0.10.2 - 2.0.6 - 5.0.0 - 3.19.2 - - true - - - - - Tim Molter - - - - - - MIT - http://www.opensource.org/licenses/mit-license.php - repo - All source code is under the MIT license. - - - - - GitHub - https://github.com/knowm/XChange/issues - - - - - sonatype-nexus-snapshots - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots - - - sonatype-nexus-staging - Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - https://oss.sonatype.org/content/groups/public/org/knowm/xchange - - - - scm:git:git@github.com:knowm/XChange.git - scm:git:git@github.com:knowm/XChange.git - git@github.com:knowm/XChange.git - HEAD - - - - xchange-bankera - xchange-bibox - xchange-binance - xchange-bitbay - xchange-bitcoinaverage - xchange-bitcoincharts - xchange-bitcoincore - xchange-bitcoinde - xchange-bitcointoyou - xchange-bitfinex - xchange-bitflyer - xchange-bithumb - xchange-ascendex - xchange-bitmex - xchange-bitso - xchange-bitstamp - xchange-bittrex - xchange-bity - xchange-bitz - xchange-bl3p - xchange-bleutrade - xchange-blockchain - xchange-btcc - xchange-btcmarkets - xchange-btcturk - xchange-bybit - xchange-ccex - xchange-cexio - xchange-coinbase - xchange-coinbasepro - xchange-coincheck - xchange-coindeal - xchange-coindirect - xchange-coinjar - xchange-coinegg - xchange-coinex - xchange-coinone - xchange-coinfloor - xchange-coingi - xchange-coinmarketcap - xchange-coinmate - xchange-core - xchange-krakenfutures - xchange-cryptowatch - xchange-deribit - xchange-dvchain - xchange-dydx - xchange-exmo - xchange-examples - xchange-ftx - xchange-gateio - xchange-globitex - xchange-gemini - xchange-hitbtc - xchange-huobi - xchange-idex - xchange-independentreserve - xchange-itbit - xchange-koineks - xchange-koinim - xchange-kraken - xchange-kucoin - xchange-kuna - xchange-lgo - xchange-latoken - xchange-livecoin - xchange-luno - xchange-lykke - xchange-mercadobitcoin - xchange-mexc - xchange-okcoin - xchange-okex - xchange-openexchangerates - xchange-paribu - xchange-paymium - xchange-poloniex - xchange-quoine - xchange-ripple - xchange-serum - xchange-simulated - xchange-therock - xchange-tradeogre - xchange-truefx - xchange-upbit - xchange-vaultoro - xchange-yobit - xchange-zaif - xchange-enigma - - - xchange-stream-bankera - xchange-stream-binance - xchange-stream-bitfinex - xchange-stream-bitflyer - xchange-stream-bitmex - xchange-stream-bitstamp - xchange-stream-btcmarkets - xchange-stream-cexio - xchange-stream-coinbasepro - xchange-stream-coinjar - xchange-stream-coinmate - xchange-stream-core - xchange-stream-dydx - xchange-stream-ftx - xchange-stream-gateio - xchange-stream-gemini - xchange-stream-gemini-v2 - xchange-stream-hitbtc - xchange-stream-huobi - xchange-stream-kraken - xchange-stream-kucoin - xchange-stream-lgo - xchange-stream-okcoin - xchange-stream-okex - xchange-stream-poloniex2 - xchange-stream-serum - xchange-stream-service-core - xchange-stream-service-netty - xchange-stream-service-pubnub - xchange-stream-coincheck - xchange-stream-krakenfutures - - - - https://travis-ci.org/github/knowm/XChange - - - - - sonatype-oss-public - https://oss.sonatype.org/content/groups/public/ - - true - - - true - - - - - + + + 4.0.0 + + + 3.0 + + + org.knowm.xchange + xchange-parent + 5.1.1-SNAPSHOT + pom + + XChange + XChange is a Java library providing a simple and consistent API for interacting with + a diverse set of cryptocurrency exchanges. + + + http://knowm.org/open-source/xchange + 2012 + + + + Knowm Inc. + http://knowm.org/open-source/xchange/ + + + + UTF-8 + UTF-8 + + 1.8 + 3.23.1 + 3.12.0 + 2.14.1 + 2.1.0 + 1.7.0 + 4.13.2 + 1.18.22 + 3.8.2 + 1.4.5 + 0.10.2 + 2.0.6 + 5.0.0 + 3.19.2 + + true + + + + + Tim Molter + + + + + + MIT + http://www.opensource.org/licenses/mit-license.php + repo + All source code is under the MIT license. + + + + + GitHub + https://github.com/knowm/XChange/issues + + + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + https://oss.sonatype.org/content/groups/public/org/knowm/xchange + + + + scm:git:git@github.com:knowm/XChange.git + scm:git:git@github.com:knowm/XChange.git + git@github.com:knowm/XChange.git + HEAD + + + + xchange-bankera + xchange-bibox + xchange-binance + xchange-bitbay + xchange-bitcoinaverage + xchange-bitcoincharts + xchange-bitcoincore + xchange-bitcoinde + xchange-bitcointoyou + xchange-bitfinex + xchange-bitflyer + xchange-bithumb + xchange-ascendex + xchange-bitmex + xchange-bitso + xchange-bitstamp + xchange-bittrex + xchange-bity + xchange-bitz + xchange-bl3p + xchange-bleutrade + xchange-blockchain + xchange-btcc + xchange-btcmarkets + xchange-btcturk + xchange-bybit + xchange-ccex + xchange-cexio + xchange-coinbase + xchange-coinbasepro + xchange-coincheck + xchange-coindeal + xchange-coindirect + xchange-coinjar + xchange-coinegg + xchange-coinex + xchange-coinone + xchange-coinfloor + xchange-coingi + xchange-coinmarketcap + xchange-coinmate + xchange-core + xchange-krakenfutures + xchange-cryptowatch + xchange-deribit + xchange-dvchain + xchange-dydx + xchange-exmo + xchange-examples + xchange-ftx + xchange-gateio + xchange-globitex + xchange-gemini + xchange-hitbtc + xchange-huobi + xchange-idex + xchange-independentreserve + xchange-itbit + xchange-koineks + xchange-koinim + xchange-kraken + xchange-kucoin + xchange-kuna + xchange-lgo + xchange-latoken + xchange-livecoin + xchange-luno + xchange-lykke + xchange-mercadobitcoin + xchange-mexc + xchange-okcoin + xchange-okex + xchange-openexchangerates + xchange-paribu + xchange-paymium + xchange-poloniex + xchange-quoine + xchange-ripple + xchange-serum + xchange-simulated + xchange-therock + xchange-tradeogre + xchange-truefx + xchange-upbit + xchange-vaultoro + xchange-yobit + xchange-zaif + xchange-enigma + + + xchange-stream-bankera + xchange-stream-binance + xchange-stream-bitfinex + xchange-stream-bitflyer + xchange-stream-bitmex + xchange-stream-bitstamp + xchange-stream-btcmarkets + xchange-stream-cexio + xchange-stream-coinbasepro + xchange-stream-coinjar + xchange-stream-coinmate + xchange-stream-core + xchange-stream-dydx + xchange-stream-ftx + xchange-stream-gateio + xchange-stream-gemini + xchange-stream-gemini-v2 + xchange-stream-hitbtc + xchange-stream-huobi + xchange-stream-kraken + xchange-stream-kucoin + xchange-stream-lgo + xchange-stream-okcoin + xchange-stream-okex + xchange-stream-poloniex2 + xchange-stream-serum + xchange-stream-service-core + xchange-stream-service-netty + xchange-stream-service-pubnub + xchange-stream-coincheck + xchange-stream-krakenfutures + xchange-stream-vertex + + + + https://travis-ci.org/github/knowm/XChange + + + + + sonatype-oss-public + https://oss.sonatype.org/content/groups/public/ + + true + + + true + + + + + + + + + + com.github.mmazi + rescu + ${version.github.mmazi} + + + commons-codec + commons-codec + + + + + + io.github.resilience4j + resilience4j-ratelimiter + ${version.resilience4j} + + + + io.github.resilience4j + resilience4j-retry + ${version.resilience4j} + + + + + org.apache.commons + commons-lang3 + ${version.commons.lang3} + + + + + org.knowm.xchart + xchart + ${version.knowm.xchart} + + + + + org.reflections + reflections + ${version.reflections} + test + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-csv + ${version.fasterxml} + + + com.fasterxml.jackson.core + jackson-databind + ${version.fasterxml} + + + com.fasterxml.jackson.core + jackson-annotations + ${version.fasterxml} + + + com.fasterxml.jackson.core + jackson-core + ${version.fasterxml} + + + org.web3j + crypto + ${version.crypto} + + + + com.google.guava + guava + 31.1-jre + + + + org.mockito + mockito-core + 3.12.4 + test + + + + com.github.tomakehurst + wiremock-jre8 + 2.35.0 + test + + + + + ch.qos.logback + logback-classic + ${version.qos.logback} + + + + + org.projectlombok + lombok + ${version.lombok} + provided + + + + + com.auth0 + java-jwt + ${version.java-jwt} + + + + + io.reactivex.rxjava2 + rxjava + 2.2.21 + + + com.pubnub + pubnub-gson + 4.31.3 + + + + io.netty + netty-all + 4.1.86.Final + + + + + + + + + + org.slf4j + slf4j-api + ${version.slf4j} + + + + + javax.annotation + javax.annotation-api + 1.3.2 + - - - com.github.mmazi - rescu - ${version.github.mmazi} - - - commons-codec - commons-codec - - - - - - io.github.resilience4j - resilience4j-ratelimiter - ${version.resilience4j} - - - - io.github.resilience4j - resilience4j-retry - ${version.resilience4j} - - - - - org.apache.commons - commons-lang3 - ${version.commons.lang3} - - - - - org.knowm.xchart - xchart - ${version.knowm.xchart} - - - - - org.reflections - reflections - ${version.reflections} - test - - - - com.fasterxml.jackson.dataformat - jackson-dataformat-csv - ${version.fasterxml} - - - com.fasterxml.jackson.core - jackson-databind - ${version.fasterxml} - + - com.fasterxml.jackson.core - jackson-annotations - ${version.fasterxml} + ch.qos.logback + logback-classic + test + + - com.fasterxml.jackson.core - jackson-core - ${version.fasterxml} + junit + junit + ${version.junit} + test - - org.web3j - crypto - ${version.crypto} - - - - com.google.guava - guava - 31.1-jre - - - - org.mockito - mockito-core - 3.12.4 - test - - - - com.github.tomakehurst - wiremock-jre8 - 2.35.0 - test - - - - - ch.qos.logback - logback-classic - ${version.qos.logback} - - - - - org.projectlombok - lombok - ${version.lombok} - provided - - - - - com.auth0 - java-jwt - ${version.java-jwt} - - - - - io.reactivex.rxjava2 - rxjava - 2.2.21 - - - com.pubnub - pubnub-gson - 4.31.3 - - - io.netty - netty-all - 4.1.86.Final + org.assertj + assertj-core + ${version.assertj} + test - - - - - - - org.slf4j - slf4j-api - ${version.slf4j} - - - - - javax.annotation - javax.annotation-api - 1.3.2 - - - - - ch.qos.logback - logback-classic - test - - - - - junit - junit - ${version.junit} - test - - - org.assertj - assertj-core - ${version.assertj} - test - - - - - - - release-sign-artifacts - - - gpg.passphrase - true - - - + + + release-sign-artifacts + + + gpg.passphrase + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + + attach-javadocs + + jar + + + none + + + + + + + + + + + + - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - - - - - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.4.1 - - - attach-javadocs - - jar - + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + ${version.java} + ${version.java} + true + true + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + true + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + + attach-javadocs + + jar + + + + + true + none + ${version.java} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M7 + + + + integration-test + verify + + + + + ${skipIntegrationTests} + + **/*Integration.java + + + + + com.coveo + fmt-maven-plugin + 2.13 - none + .*\.java + false - - - + + + + + + + + - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.10.1 - - ${version.java} - ${version.java} - true - true - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - true - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.4.1 - - - attach-javadocs - - jar - - - - - true - none - ${version.java} - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M7 - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.0.0-M7 - - - - integration-test - verify - - - - - ${skipIntegrationTests} - - **/*Integration.java - - - - - com.coveo - fmt-maven-plugin - 2.13 - - .*\.java - false - - - - - - - - - - - - + - diff --git a/xchange-examples/pom.xml b/xchange-examples/pom.xml index b83acfbc78a..207c9341a72 100755 --- a/xchange-examples/pom.xml +++ b/xchange-examples/pom.xml @@ -1,367 +1,372 @@ - 4.0.0 + 4.0.0 - - org.knowm.xchange - xchange-parent - 5.1.1-SNAPSHOT - + + org.knowm.xchange + xchange-parent + 5.1.1-SNAPSHOT + - xchange-examples + xchange-examples - XChange Examples - Provides a set of examples that demonstrate how to use XChange in client applications + XChange Examples + Provides a set of examples that demonstrate how to use XChange in client applications - http://knowm.org/open-source/xchange/ - 2012 + http://knowm.org/open-source/xchange/ + 2012 - - Knowm Inc. - http://knowm.org/open-source/xchange/ - + + Knowm Inc. + http://knowm.org/open-source/xchange/ + - - + + - - ch.qos.logback - logback-classic - - - org.knowm.xchart - xchart - + + ch.qos.logback + logback-classic + + + org.knowm.xchart + xchart + - - org.reflections - reflections - + + org.reflections + reflections + - - - ${project.groupId} - xchange-core - ${project.version} - - - - ${project.groupId} - xchange-openexchangerates - ${project.version} - - - - ${project.groupId} - xchange-bibox - ${project.version} - - - - ${project.groupId} - xchange-binance - ${project.version} - - - - ${project.groupId} - xchange-bithumb - ${project.version} - - - - ${project.groupId} - xchange-bitstamp - ${project.version} - - - - ${project.groupId} - xchange-bittrex - ${project.version} - - - - ${project.groupId} - xchange-bitcoincharts - ${project.version} - - - - ${project.groupId} - xchange-bitcoinde - ${project.version} - - - - ${project.groupId} - xchange-blockchain - ${project.version} - - - - ${project.groupId} - xchange-kraken - ${project.version} - - - - ${project.groupId} - xchange-kucoin - ${project.version} - - - - ${project.groupId} - xchange-bitfinex - ${project.version} - - - - ${project.groupId} - xchange-bitmex - ${project.version} - - - - ${project.groupId} - xchange-coinbase - ${project.version} - - - - ${project.groupId} - xchange-coincheck - ${project.version} - - - - ${project.groupId} - xchange-coinegg - ${project.version} - - - - ${project.groupId} - xchange-coinbasepro - ${project.version} - - - - ${project.groupId} - xchange-bitcoinaverage - ${project.version} - - - - ${project.groupId} - xchange-cexio - ${project.version} - - - - ${project.groupId} - xchange-coingi - ${project.version} - - - - ${project.groupId} - xchange-huobi - ${project.version} - - - - ${project.groupId} - xchange-itbit - ${project.version} - - - - ${project.groupId} - xchange-independentreserve - ${project.version} - - - - ${project.groupId} - xchange-hitbtc - ${project.version} - + + + ${project.groupId} + xchange-core + ${project.version} + + + + ${project.groupId} + xchange-openexchangerates + ${project.version} + + + + ${project.groupId} + xchange-bibox + ${project.version} + + + + ${project.groupId} + xchange-binance + ${project.version} + + + + ${project.groupId} + xchange-bithumb + ${project.version} + + + + ${project.groupId} + xchange-bitstamp + ${project.version} + + + + ${project.groupId} + xchange-bittrex + ${project.version} + + + + ${project.groupId} + xchange-bitcoincharts + ${project.version} + + + + ${project.groupId} + xchange-bitcoinde + ${project.version} + + + + ${project.groupId} + xchange-blockchain + ${project.version} + + + + ${project.groupId} + xchange-kraken + ${project.version} + + + + ${project.groupId} + xchange-kucoin + ${project.version} + + + + ${project.groupId} + xchange-bitfinex + ${project.version} + + + + ${project.groupId} + xchange-bitmex + ${project.version} + + + + ${project.groupId} + xchange-coinbase + ${project.version} + + + + ${project.groupId} + xchange-coincheck + ${project.version} + + + + ${project.groupId} + xchange-coinegg + ${project.version} + + + + ${project.groupId} + xchange-coinbasepro + ${project.version} + + + + ${project.groupId} + xchange-bitcoinaverage + ${project.version} + + + + ${project.groupId} + xchange-cexio + ${project.version} + + + + ${project.groupId} + xchange-coingi + ${project.version} + + + + ${project.groupId} + xchange-huobi + ${project.version} + + + + ${project.groupId} + xchange-itbit + ${project.version} + + + + ${project.groupId} + xchange-independentreserve + ${project.version} + + + + ${project.groupId} + xchange-hitbtc + ${project.version} + - - - ${project.groupId} - xchange-paymium - ${project.version} - - - - ${project.groupId} - xchange-poloniex - ${project.version} - - - - ${project.groupId} - xchange-okcoin - ${project.version} - - - - ${project.groupId} - xchange-okex - ${project.version} - - - - ${project.groupId} - xchange-bleutrade - ${project.version} - - - - ${project.groupId} - xchange-mercadobitcoin - ${project.version} - - - - ${project.groupId} - xchange-bitbay - ${project.version} - - - - ${project.groupId} - xchange-bitso - ${project.version} - - - ${project.groupId} - xchange-quoine - ${project.version} - - - ${project.groupId} - xchange-ripple - ${project.version} - - - ${project.groupId} - xchange-therock - ${project.version} - - - ${project.groupId} - xchange-btcmarkets - ${project.version} - - - ${project.groupId} - xchange-krakenfutures - ${project.version} - - - ${project.groupId} - xchange-coinmate - ${project.version} - - - ${project.groupId} - xchange-coinone - ${project.version} - - - ${project.groupId} - xchange-upbit - ${project.version} - - - ${project.groupId} - xchange-ccex - ${project.version} - - - ${project.groupId} - xchange-livecoin - ${project.version} - - - ${project.groupId} - xchange-yobit - ${project.version} - - - ${project.groupId} - xchange-btcturk - ${project.version} - - - ${project.groupId} - xchange-paribu - ${project.version} - - - ${project.groupId} - xchange-koineks - ${project.version} - - - ${project.groupId} - xchange-gateio - ${project.version} - - - ${project.groupId} - xchange-koinim - ${project.version} - - - ${project.groupId} - xchange-bitflyer - ${project.version} - - - ${project.groupId} - xchange-bitz - ${project.version} - - - ${project.groupId} - xchange-zaif - ${project.version} - - - ${project.groupId} - xchange-coindirect - ${project.version} - - - ${project.groupId} - xchange-dvchain - ${project.version} - - - ${project.groupId} - xchange-bankera - ${project.version} - + + + ${project.groupId} + xchange-paymium + ${project.version} + + + + ${project.groupId} + xchange-poloniex + ${project.version} + + + + ${project.groupId} + xchange-okcoin + ${project.version} + + + + ${project.groupId} + xchange-okex + ${project.version} + + + + ${project.groupId} + xchange-bleutrade + ${project.version} + + + + ${project.groupId} + xchange-mercadobitcoin + ${project.version} + + + + ${project.groupId} + xchange-bitbay + ${project.version} + + + + ${project.groupId} + xchange-bitso + ${project.version} + + + ${project.groupId} + xchange-quoine + ${project.version} + + + ${project.groupId} + xchange-ripple + ${project.version} + + + ${project.groupId} + xchange-therock + ${project.version} + + + ${project.groupId} + xchange-btcmarkets + ${project.version} + + + ${project.groupId} + xchange-krakenfutures + ${project.version} + + + ${project.groupId} + xchange-coinmate + ${project.version} + + + ${project.groupId} + xchange-coinone + ${project.version} + + + ${project.groupId} + xchange-upbit + ${project.version} + + + ${project.groupId} + xchange-ccex + ${project.version} + + + ${project.groupId} + xchange-livecoin + ${project.version} + + + ${project.groupId} + xchange-yobit + ${project.version} + + + ${project.groupId} + xchange-btcturk + ${project.version} + + + ${project.groupId} + xchange-paribu + ${project.version} + + + ${project.groupId} + xchange-koineks + ${project.version} + + + ${project.groupId} + xchange-gateio + ${project.version} + + + ${project.groupId} + xchange-koinim + ${project.version} + + + ${project.groupId} + xchange-bitflyer + ${project.version} + + + ${project.groupId} + xchange-bitz + ${project.version} + + + ${project.groupId} + xchange-zaif + ${project.version} + + + ${project.groupId} + xchange-coindirect + ${project.version} + + + ${project.groupId} + xchange-dvchain + ${project.version} + + + ${project.groupId} + xchange-bankera + ${project.version} + org.knowm.xchange xchange-enigma ${project.version} - - org.knowm.xchange - xchange-lgo - ${project.version} - - - ${project.groupId} - xchange-deribit - ${project.version} - - + + org.knowm.xchange + xchange-lgo + ${project.version} + + + ${project.groupId} + xchange-deribit + ${project.version} + + + ${project.groupId} + xchange-stream-vertex + ${project.version} + + diff --git a/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexMarketDataExample.java b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexMarketDataExample.java new file mode 100644 index 00000000000..6a2a0ab042f --- /dev/null +++ b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexMarketDataExample.java @@ -0,0 +1,87 @@ +package org.knowm.xchange.examples.vertex; + +import com.knowm.xchange.vertex.VertexStreamingExchange; +import info.bitrich.xchangestream.core.StreamingExchange; +import info.bitrich.xchangestream.core.StreamingExchangeFactory; +import info.bitrich.xchangestream.core.StreamingMarketDataService; +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import java.util.concurrent.atomic.AtomicLong; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.OrderBook; +import org.knowm.xchange.dto.marketdata.Trade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; + +public class VertexMarketDataExample { + + private static final Logger logger = LoggerFactory.getLogger(VertexMarketDataExample.class); + + + public static void main(String[] args) throws InterruptedException { + ExchangeSpecification exchangeSpecification = new ExchangeSpecification(VertexStreamingExchange.class); + + String privateKey = System.getProperty("WALLET_PRIVATE_KEY"); + ECKeyPair ecKeyPair = Credentials.create(privateKey).getEcKeyPair(); + String address = "0x" + Keys.getAddress(ecKeyPair.getPublicKey()); + + exchangeSpecification.setApiKey(address); + exchangeSpecification.setSecretKey(privateKey); + + exchangeSpecification.setExchangeSpecificParametersItem(StreamingExchange.USE_SANDBOX, true); + + StreamingExchange exchange = StreamingExchangeFactory.INSTANCE.createExchange(exchangeSpecification); + + exchange.connect().blockingAwait(); + + CurrencyPair btcUsdc = new CurrencyPair(Currency.BTC, Currency.USDC); + CurrencyPair ethUsdc = new CurrencyPair(Currency.ETH, Currency.USDC); + + Disposable ticker = exchange.getStreamingMarketDataService().getTicker(btcUsdc) + .forEach(tick -> logger.info(btcUsdc + " TOB: " + tick)); + + Disposable disconnectBtcTOB = subscribe(exchange.getStreamingMarketDataService(), btcUsdc.toString(), 1); + Disposable disconnectBtc15 = subscribe(exchange.getStreamingMarketDataService(), btcUsdc.toString(), 15); + + Disposable disconnectEth = subscribe(exchange.getStreamingMarketDataService(), ethUsdc.toString(), 2); + + logger.info("\n\n Disconnecting 15 depth BTC-USDC \n\n"); + + disconnectBtc15.dispose(); + + logger.info("\n\n Disconnecting ETH-USDC \n\n"); + + disconnectEth.dispose(); + + Thread.sleep(10000); + + disconnectBtcTOB.dispose(); + + ticker.dispose(); + + exchange.disconnect().blockingAwait(); + + + } + + public static Disposable subscribe(StreamingMarketDataService streamingMarketDataService, String instrument, int depth) { + CurrencyPair currencyPair = new CurrencyPair(instrument); + + Observable orderBook = streamingMarketDataService.getOrderBook(currencyPair, depth); + + AtomicLong counter = new AtomicLong(0); + Disposable disconnectMarketData = orderBook.subscribe(book -> logger.info("Received book update for instrument {}, depth {} #{} {}", instrument, depth, counter.incrementAndGet(), book)); + + Observable trades = streamingMarketDataService.getTrades(currencyPair); + Disposable disconnectTrades = trades.subscribe(trade -> logger.info("Received trade update for instrument {}: {}", instrument, trade)); + + + return new CompositeDisposable(disconnectMarketData, disconnectTrades); + } +} diff --git a/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexOrderExample.java b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexOrderExample.java new file mode 100644 index 00000000000..f203bb51bae --- /dev/null +++ b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexOrderExample.java @@ -0,0 +1,133 @@ +package org.knowm.xchange.examples.vertex; + +import com.knowm.xchange.vertex.VertexOrderFlags; +import com.knowm.xchange.vertex.VertexStreamingExchange; +import com.knowm.xchange.vertex.VertexStreamingTradeService; +import com.knowm.xchange.vertex.dto.RewardsList; +import info.bitrich.xchangestream.core.StreamingExchange; +import info.bitrich.xchangestream.core.StreamingExchangeFactory; +import io.reactivex.disposables.Disposable; +import java.io.IOException; +import java.math.BigDecimal; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.dto.trade.MarketOrder; +import org.knowm.xchange.service.trade.params.CancelAllOrders; +import org.knowm.xchange.service.trade.params.DefaultCancelAllOrdersByInstrument; +import org.knowm.xchange.service.trade.params.DefaultCancelOrderByInstrumentAndIdParams; +import org.knowm.xchange.service.trade.params.orders.DefaultOpenOrdersParamInstrument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; +import org.web3j.utils.Numeric; + +public class VertexOrderExample { + + private static final Logger log = LoggerFactory.getLogger(VertexOrderExample.class); + + + public static void main(String[] args) throws IOException, InterruptedException { + + ExchangeSpecification exchangeSpecification = StreamingExchangeFactory.INSTANCE + .createExchangeWithoutSpecification(VertexStreamingExchange.class) + .getDefaultExchangeSpecification(); + + + ECKeyPair ecKeyPair = Credentials.create(System.getProperty("WALLET_PRIVATE_KEY")).getEcKeyPair(); + String address = "0x" + Keys.getAddress(ecKeyPair.getPublicKey()); + String subAccount = "default"; + + exchangeSpecification.setApiKey(address); + exchangeSpecification.setSecretKey(Numeric.toHexStringNoPrefix(ecKeyPair.getPrivateKey())); + exchangeSpecification.setExchangeSpecificParametersItem(StreamingExchange.USE_SANDBOX, true); + exchangeSpecification.setExchangeSpecificParametersItem(VertexStreamingExchange.USE_LEVERAGE, true); + + exchangeSpecification.setUserName(subAccount); //subaccount name + + VertexStreamingExchange exchange = (VertexStreamingExchange) StreamingExchangeFactory.INSTANCE.createExchange(exchangeSpecification); + + exchange.connect().blockingAwait(); + + + RewardsList rewardsList = exchange.queryRewards(address); + System.out.println(rewardsList); + + VertexStreamingTradeService tradeService = exchange.getStreamingTradeService(); + + + CurrencyPair btc = new CurrencyPair("BTC-PERP", "USDC"); + + Disposable trades = tradeService.getUserTrades(btc, subAccount).subscribe(userTrade -> { + log.info("User trade: {}", userTrade); + }); + + Disposable changes = tradeService.getOrderChanges(btc, subAccount).subscribe(order -> { + log.info("User order event: {}", order); + }); + + MarketOrder buy = new MarketOrder(Order.OrderType.BID, BigDecimal.valueOf(0.01), btc); + buy.addOrderFlag(VertexOrderFlags.TIME_IN_FORCE_IOC); + tradeService.placeMarketOrder(buy); + + Thread.sleep(2000); + + log.info("Open positions before sell: {}", tradeService.getOpenPositions()); + + MarketOrder sell = new MarketOrder(Order.OrderType.ASK, BigDecimal.valueOf(0.01), btc); + sell.addOrderFlag(VertexOrderFlags.TIME_IN_FORCE_FOK); + tradeService.placeMarketOrder(sell); + + LimitOrder resting = new LimitOrder(Order.OrderType.BID, BigDecimal.valueOf(0.01), btc, null, null, BigDecimal.valueOf(20000)); + String orderId = tradeService.placeLimitOrder(resting); + + Thread.sleep(5000); + + log.info("Open orders before cancel: {}", tradeService.getOpenOrders(new DefaultOpenOrdersParamInstrument(btc))); + log.info("Open positions before cancel: {}", tradeService.getOpenPositions()); + + tradeService.cancelOrder(new DefaultCancelOrderByInstrumentAndIdParams(btc, orderId)); + + log.info("Open orders after cancel: {}", tradeService.getOpenOrders(new DefaultOpenOrdersParamInstrument(btc))); + + // Check leveraged shorting works + sell = new MarketOrder(Order.OrderType.ASK, BigDecimal.valueOf(0.01), btc); + sell.addOrderFlag(VertexOrderFlags.TIME_IN_FORCE_FOK); + tradeService.placeMarketOrder(sell); + + buy = new MarketOrder(Order.OrderType.BID, BigDecimal.valueOf(0.01), btc); + buy.addOrderFlag(VertexOrderFlags.TIME_IN_FORCE_IOC); + tradeService.placeMarketOrder(buy); + + Thread.sleep(2000); + + LimitOrder resting2 = new LimitOrder(Order.OrderType.BID, BigDecimal.valueOf(0.01), btc, null, null, BigDecimal.valueOf(20000)); + String orderId2 = tradeService.placeLimitOrder(resting); + + + log.info("Open orders before cancel all instrument: {}", tradeService.getOpenOrders()); + + tradeService.cancelOrder(new DefaultCancelAllOrdersByInstrument(btc)); + + log.info("Open orders after cancel: {}", tradeService.getOpenOrders(new DefaultOpenOrdersParamInstrument(btc))); + + + LimitOrder resting3 = new LimitOrder(Order.OrderType.ASK, BigDecimal.valueOf(0.01), btc, null, null, BigDecimal.valueOf(40000)); + String orderId3 = tradeService.placeLimitOrder(resting); + + + log.info("Open orders before cancel all instrument: {}", tradeService.getOpenOrders(new DefaultOpenOrdersParamInstrument(btc))); + + tradeService.cancelOrder(new CancelAllOrders() { + }); + + + log.info("Open orders after cancel: {}", tradeService.getOpenOrders(new DefaultOpenOrdersParamInstrument(btc))); + + exchange.disconnect().blockingAwait(); + + } +} diff --git a/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexRewardsExample.java b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexRewardsExample.java new file mode 100644 index 00000000000..6e04d5dca13 --- /dev/null +++ b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexRewardsExample.java @@ -0,0 +1,50 @@ +package org.knowm.xchange.examples.vertex; + +import com.knowm.xchange.vertex.VertexStreamingExchange; +import com.knowm.xchange.vertex.dto.RewardsList; +import info.bitrich.xchangestream.core.StreamingExchange; +import info.bitrich.xchangestream.core.StreamingExchangeFactory; +import java.io.IOException; +import org.knowm.xchange.ExchangeSpecification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; +import org.web3j.utils.Numeric; + +public class VertexRewardsExample { + + private static final Logger log = LoggerFactory.getLogger(VertexRewardsExample.class); + + + public static void main(String[] args) throws IOException, InterruptedException { + + ExchangeSpecification exchangeSpecification = StreamingExchangeFactory.INSTANCE + .createExchangeWithoutSpecification(VertexStreamingExchange.class) + .getDefaultExchangeSpecification(); + + + ECKeyPair ecKeyPair = Credentials.create(System.getProperty("WALLET_PRIVATE_KEY")).getEcKeyPair(); + String address = "0x" + Keys.getAddress(ecKeyPair.getPublicKey()); + String subAccount = "default"; + + exchangeSpecification.setApiKey(address); + exchangeSpecification.setSecretKey(Numeric.toHexStringNoPrefix(ecKeyPair.getPrivateKey())); + exchangeSpecification.setExchangeSpecificParametersItem(StreamingExchange.USE_SANDBOX, true); + exchangeSpecification.setExchangeSpecificParametersItem(VertexStreamingExchange.USE_LEVERAGE, true); + + exchangeSpecification.setUserName(subAccount); //subaccount name + + VertexStreamingExchange exchange = (VertexStreamingExchange) StreamingExchangeFactory.INSTANCE.createExchange(exchangeSpecification); + + exchange.connect().blockingAwait(); + + log.info("Querying rewards for address: " + address); + RewardsList rewardsList = exchange.queryRewards(address); + log.info("Response: " + rewardsList); + + exchange.disconnect().blockingAwait(); + + } +} diff --git a/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexTickerExample.java b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexTickerExample.java new file mode 100644 index 00000000000..88681d57d8d --- /dev/null +++ b/xchange-examples/src/main/java/org/knowm/xchange/examples/vertex/VertexTickerExample.java @@ -0,0 +1,40 @@ +package org.knowm.xchange.examples.vertex; + +import com.knowm.xchange.vertex.VertexStreamingExchange; +import info.bitrich.xchangestream.core.StreamingExchange; +import info.bitrich.xchangestream.core.StreamingExchangeFactory; +import io.reactivex.disposables.Disposable; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.currency.CurrencyPair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VertexTickerExample { + + private static final Logger logger = LoggerFactory.getLogger(VertexTickerExample.class); + public static final String BTC_USDC = "wBTC-USDC"; + + public static void main(String[] args) throws InterruptedException { + ExchangeSpecification exchangeSpecification = new ExchangeSpecification(VertexStreamingExchange.class); + + exchangeSpecification.setApiKey("YOUR_WALLET_ADDRESS"); + exchangeSpecification.setSecretKey("YOUR_WALLET_SECRET_KEY"); + exchangeSpecification.setExchangeSpecificParametersItem(StreamingExchange.USE_SANDBOX, true); + + StreamingExchange exchange = StreamingExchangeFactory.INSTANCE.createExchange(exchangeSpecification); + + exchange.connect().blockingAwait(); + + Disposable ticker = exchange.getStreamingMarketDataService().getTicker(new CurrencyPair(BTC_USDC)) + .forEach(tick -> logger.info(BTC_USDC + " TOB: " + tick)); + + Thread.sleep(30000); + + ticker.dispose(); + + exchange.disconnect().blockingAwait(); + + + } + +} diff --git a/xchange-stream-service-netty/src/main/java/info/bitrich/xchangestream/service/netty/NettyStreamingService.java b/xchange-stream-service-netty/src/main/java/info/bitrich/xchangestream/service/netty/NettyStreamingService.java index 925d9ad1e53..02a6ff087dc 100644 --- a/xchange-stream-service-netty/src/main/java/info/bitrich/xchangestream/service/netty/NettyStreamingService.java +++ b/xchange-stream-service-netty/src/main/java/info/bitrich/xchangestream/service/netty/NettyStreamingService.java @@ -10,6 +10,7 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; @@ -34,11 +35,13 @@ import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.ScheduledFuture; import io.netty.util.internal.SocketUtils; import io.netty.util.internal.StringUtil; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.ObservableEmitter; +import io.reactivex.disposables.Disposable; import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.Subject; import java.io.IOException; @@ -50,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +63,7 @@ public abstract class NettyStreamingService extends ConnectableService { protected static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(10); protected static final Duration DEFAULT_RETRY_DURATION = Duration.ofSeconds(15); protected static final int DEFAULT_IDLE_TIMEOUT = 15; + private ScheduledFuture scheduledReconnection; protected class Subscription { @@ -84,7 +89,9 @@ public ObservableEmitter getEmitter() { private final Duration retryDuration; private final Duration connectionTimeout; private final int idleTimeoutSeconds; - private volatile NioEventLoopGroup eventLoopGroup; + private Supplier eventLoopGroupFactory = () -> new NioEventLoopGroup(2); + private volatile EventLoopGroup eventLoopGroup; + private Class socketChannelClass = NioSocketChannel.class; protected final Map channels = new ConcurrentHashMap<>(); private boolean compressedMessages = false; @@ -191,7 +198,7 @@ protected Completable openConnection() { this::messageHandler); if (eventLoopGroup == null || eventLoopGroup.isShutdown()) { - eventLoopGroup = new NioEventLoopGroup(2); + eventLoopGroup = eventLoopGroupFactory.get(); } new Bootstrap() @@ -200,7 +207,7 @@ protected Completable openConnection() { ChannelOption.CONNECT_TIMEOUT_MILLIS, java.lang.Math.toIntExact(connectionTimeout.toMillis())) .option(ChannelOption.SO_KEEPALIVE, true) - .channel(NioSocketChannel.class) + .channel(socketChannelClass) .handler( new ChannelInitializer() { @Override @@ -281,10 +288,11 @@ protected void initChannel(SocketChannel ch) { } private void scheduleReconnect() { - if (autoReconnect) { - LOG.info("Scheduling reconnection"); + if (autoReconnect && !isManualDisconnect.get()) { + LOG.info("Scheduling reconnection to " + uri.toString() + " in " + retryDuration.toMillis() + "ms"); + if (scheduledReconnection != null) scheduledReconnection.cancel(true); - webSocketChannel + scheduledReconnection = webSocketChannel .eventLoop() .schedule( () -> @@ -302,7 +310,11 @@ protected DefaultHttpHeaders getCustomHeaders() { } public Completable disconnect() { - isManualDisconnect.set(true); + return disconnect(false); + } + + public Completable disconnect(boolean autoReconnect) { + isManualDisconnect.set(!autoReconnect); return Completable.create( completable -> { if (webSocketChannel != null && webSocketChannel.isOpen()) { @@ -311,7 +323,7 @@ public Completable disconnect() { .writeAndFlush(closeFrame) .addListener( future -> { - channels.clear(); + if (autoReconnect) channels.clear(); eventLoopGroup .shutdownGracefully(2, idleTimeoutSeconds, TimeUnit.SECONDS) .addListener( @@ -359,7 +371,6 @@ public String getSubscriptionUniqueId(String channelName, Object... args) { public abstract void messageHandler(String message); public void sendMessage(String message) { - LOG.debug("Sending message: {}", message); if (webSocketChannel == null || !webSocketChannel.isOpen()) { LOG.warn("WebSocket is not open! Call connect first."); @@ -371,6 +382,7 @@ public void sendMessage(String message) { return; } if (message != null) { + LOG.debug("Sending message: {}", message); webSocketChannel.writeAndFlush(new TextWebSocketFrame(message)); } } @@ -419,7 +431,7 @@ public Observable subscribeChannel(String channelName, Object... args) { () -> { if (channels.remove(channelId) != null) { try { - sendMessage(getUnsubscribeMessage(channelId)); + sendMessage(getUnsubscribeMessage(channelId, args)); } catch (IOException e) { LOG.debug("Failed to unsubscribe channel: {} {}", channelId, e.toString()); } catch (Exception e) { @@ -431,6 +443,7 @@ public Observable subscribeChannel(String channelName, Object... args) { } public void resubscribeChannels() { + LOG.info("Resubscribing to {} channels on {}: {}", channels.size(), uri.toString(), channels.keySet()); for (Entry entry : channels.entrySet()) { try { Subscription subscription = entry.getValue(); @@ -538,6 +551,7 @@ public void channelInactive(ChannelHandlerContext ctx) { super.channelInactive(ctx); disconnectEmitters.onNext(new Object()); LOG.info("Reopening Websocket Client because it was closed! {}", ctx.channel()); + connectionStateModel.setState(State.CLOSED); scheduleReconnect(); } } @@ -562,6 +576,14 @@ public void useCompressedMessages(boolean compressedMessages) { this.compressedMessages = compressedMessages; } + public void setEventLoopGroupFactory(Supplier eventLoopGroupFactory) { + this.eventLoopGroupFactory = eventLoopGroupFactory; + } + + public void setSocketChannelClass(Class socketChannelClass) { + this.socketChannelClass = socketChannelClass; + } + public void setAcceptAllCertificates(boolean acceptAllCertificates) { this.acceptAllCertificates = acceptAllCertificates; } diff --git a/xchange-stream-vertex/pom.xml b/xchange-stream-vertex/pom.xml new file mode 100644 index 00000000000..445a3df6293 --- /dev/null +++ b/xchange-stream-vertex/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + + xchange-parent + org.knowm.xchange + 5.1.1-SNAPSHOT + + + xchange-stream-vertex + XChange Vertex Stream + + + + org.knowm.xchange + xchange-stream-core + ${project.version} + compile + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${version.fasterxml} + + + + org.web3j + crypto + + + + org.projectlombok + lombok + + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/InstrumentDefinition.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/InstrumentDefinition.java new file mode 100644 index 00000000000..ec6ff43ead0 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/InstrumentDefinition.java @@ -0,0 +1,18 @@ +package com.knowm.xchange.vertex; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class InstrumentDefinition { + + private final BigDecimal priceIncrement; + private final BigDecimal quantityIncrement; + + public InstrumentDefinition(BigDecimal priceIncrement, BigDecimal quantityIncrement) { + this.priceIncrement = priceIncrement; + this.quantityIncrement = quantityIncrement; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/NanoSecondsDeserializer.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/NanoSecondsDeserializer.java new file mode 100644 index 00000000000..ba0f8203bc4 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/NanoSecondsDeserializer.java @@ -0,0 +1,38 @@ +package com.knowm.xchange.vertex; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.time.Instant; + +public class NanoSecondsDeserializer extends JsonDeserializer { + + private static final BigDecimal NANOS_PER_MILLI = new BigDecimal(1000000); + + private static final CacheLoader parser = new CacheLoader<>() { + @Override + public Instant load(String str) { + BigInteger nano = new BigInteger(str); + return Instant.ofEpochMilli(new BigDecimal(nano).divide(NANOS_PER_MILLI, RoundingMode.FLOOR).longValue()); + } + }; + + public static Instant parse(String str) { + return instantCache.getUnchecked(str); + } + + private static final LoadingCache instantCache = CacheBuilder.newBuilder().maximumSize(1000).build(parser); + + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return instantCache.getUnchecked(p.getValueAsString()); + + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/Query.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/Query.java new file mode 100644 index 00000000000..f563f5114a8 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/Query.java @@ -0,0 +1,29 @@ +package com.knowm.xchange.vertex; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class Query { + private final String queryMsg; + private final Consumer respHandler; + private final BiConsumer errorHandler; + + public Query(String queryMsg, Consumer respHandler, BiConsumer errorHandler) { + this.queryMsg = queryMsg; + this.respHandler = respHandler; + this.errorHandler = errorHandler; + } + + public String getQueryMsg() { + return queryMsg; + } + + public Consumer getRespHandler() { + return respHandler; + } + + public BiConsumer getErrorHandler() { + return errorHandler; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/TopOfBookPrice.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/TopOfBookPrice.java new file mode 100644 index 00000000000..dde1482a3cb --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/TopOfBookPrice.java @@ -0,0 +1,18 @@ +package com.knowm.xchange.vertex; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class TopOfBookPrice { + private final BigDecimal bid; + private final BigDecimal offer; + + public TopOfBookPrice(BigDecimal bid, BigDecimal offer) { + + this.bid = bid; + this.offer = offer; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexOrderFlags.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexOrderFlags.java new file mode 100644 index 00000000000..4db8e98c498 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexOrderFlags.java @@ -0,0 +1,11 @@ +package com.knowm.xchange.vertex; + +import org.knowm.xchange.dto.Order; + +public enum VertexOrderFlags implements Order.IOrderFlags { + + TIME_IN_FORCE_IOC, + TIME_IN_FORCE_GTC, + TIME_IN_FORCE_FOK, + TIME_IN_FORCE_POS_ONLY +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexProductInfo.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexProductInfo.java new file mode 100644 index 00000000000..e82485ab3fa --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexProductInfo.java @@ -0,0 +1,85 @@ +package com.knowm.xchange.vertex; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.knowm.xchange.vertex.dto.Symbol; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.instrument.Instrument; + +public class VertexProductInfo { + + + private final Set spotProducts; + + private final BiMap productIdToInstrument = HashBiMap.create(); + + private final Map takerFees = new HashMap<>(); + + private final Map makerFees = new HashMap<>(); + + private final BigDecimal takerSequencerFee; + + public VertexProductInfo(Set spotProducts, Symbol[] symbols, List takerFeeList, List makerFeeList, BigDecimal takerSequencerFee) { + this.spotProducts = spotProducts; + this.takerSequencerFee = takerSequencerFee; + for (Symbol symbol : symbols) { + long productId = symbol.getProduct_id(); + CurrencyPair usdcPair = new CurrencyPair(symbol.getSymbol(), "USDC"); + productIdToInstrument.put(productId, usdcPair); + } + + for (int i = 0; i < takerFeeList.size(); i++) { + BigDecimal value = takerFeeList.get(i); + if (value.compareTo(BigDecimal.ZERO) < 0) { + value = value.negate(); + } + takerFees.put((long) i, value); + } + for (int i = 0; i < makerFeeList.size(); i++) { + BigDecimal value = makerFeeList.get(i); + if (value.compareTo(BigDecimal.ZERO) < 0) { + value = value.negate(); + } + makerFees.put((long) i, value); + } + } + + long lookupProductId(Instrument currencyPair) { + Long id = productIdToInstrument.inverse().get(currencyPair); + if (id != null) { + return id; + } + throw new RuntimeException("unknown product id for " + currencyPair); + + } + + public List getProductsIds() { + return new ArrayList<>(productIdToInstrument.keySet()); + } + + public boolean isSpot(Instrument instrument) { + return spotProducts.contains(lookupProductId(instrument)); + } + + public Instrument lookupInstrument(long productId) { + return productIdToInstrument.get(productId); + } + + public BigDecimal makerTradeFee(long productId) { + return makerFees.get(productId); + } + + public BigDecimal takerTradeFee(long productId) { + return takerFees.get(productId); + } + + public BigDecimal takerSequencerFee() { + return takerSequencerFee; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingExchange.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingExchange.java new file mode 100644 index 00000000000..63c848c3f80 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingExchange.java @@ -0,0 +1,355 @@ +package com.knowm.xchange.vertex; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.base.MoreObjects; +import static com.knowm.xchange.vertex.VertexStreamingService.ALL_MESSAGES; +import com.knowm.xchange.vertex.api.VertexApi; +import com.knowm.xchange.vertex.dto.RewardsList; +import com.knowm.xchange.vertex.dto.RewardsRequest; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.buildSender; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.convertToDecimal; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.readX18Decimal; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.readX18DecimalArray; +import info.bitrich.xchangestream.core.ProductSubscription; +import info.bitrich.xchangestream.core.StreamingExchange; +import info.bitrich.xchangestream.core.StreamingMarketDataService; +import info.bitrich.xchangestream.service.netty.ConnectionStateModel; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.StringUtils; +import org.knowm.xchange.BaseExchange; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.client.ExchangeRestProxyBuilder; +import org.knowm.xchange.exceptions.ExchangeException; +import org.knowm.xchange.service.trade.TradeService; + +public class VertexStreamingExchange extends BaseExchange implements StreamingExchange { + + public static final String USE_LEVERAGE = "useLeverage"; + public static final String MAX_SLIPPAGE_RATIO = "maxSlippageRatio"; + public static final String DEFAULT_SUB_ACCOUNT = "default"; + public static final String PLACE_ORDER_VALID_UNTIL_MS_PROP = "placeOrderValidUntilMs"; + + private VertexStreamingService subscriptionStream; + private VertexStreamingMarketDataService streamingMarketDataService; + private VertexStreamingTradeService streamingTradeService; + + private boolean useTestnet; + + private long chainId; + + private String endpointContract; + + private List bookContracts; + + private VertexStreamingService requestResponseStream; + private VertexProductInfo productInfo; + + private final Map marketPrices = new ConcurrentHashMap<>(); + private final Map increments = new HashMap<>(); + + private final Set spotProducts = new TreeSet<>(); + private final Set perpProducts = new TreeSet<>(); + + private Observable allMessages; + private VertexApi restApiClient; + + + private VertexStreamingService createStreamingService(String suffix) { + VertexStreamingService streamingService = new VertexStreamingService(getApiUrl() + suffix); + applyStreamingSpecification(getExchangeSpecification(), streamingService); + + return streamingService; + } + + private String getApiUrl() { + return "wss://" + getHost(useTestnet); + + } + + @Override + public ExchangeSpecification getDefaultExchangeSpecification() { + ExchangeSpecification exchangeSpecification = new ExchangeSpecification(this.getClass()); + String host = getHost(useTestnet); + exchangeSpecification.setSslUri("https://" + host); + exchangeSpecification.setHost(host); + exchangeSpecification.setExchangeName("Vertex"); + exchangeSpecification.setExchangeDescription("Vertex - One DEX. Everything you need."); + return exchangeSpecification; + } + + private static String getHost(boolean useTestnet) { + return useTestnet ? "test.vertexprotocol-backend.com" : "prod.vertexprotocol-backend.com"; + } + + + public void applySpecification(ExchangeSpecification exchangeSpecification) { + this.useTestnet = !Boolean.FALSE.equals(exchangeSpecification.getExchangeSpecificParametersItem(USE_SANDBOX)); + + exchangeSpecification.setSslUri("https://" + getHost(useTestnet)); + + super.applySpecification(exchangeSpecification); + } + + + @Override + public void remoteInit() throws ExchangeException { + + if (!requestResponseStream.isSocketOpen() && !requestResponseStream.connect().blockingAwait(10, TimeUnit.SECONDS)) { + throw new RuntimeException("Timeout waiting for connection"); + } + + ArrayList queries = new ArrayList<>(); + ArrayList priceQueries = new ArrayList<>(); + logger.info("Loading contract data and current prices"); + queries.add(new Query("{\"type\":\"contracts\"}", + data1 -> { + chainId = Long.parseLong(data1.get("chain_id").asText()); + endpointContract = data1.get("endpoint_addr").asText(); + bookContracts = new ArrayList<>(); + data1.withArray("book_addrs").elements().forEachRemaining(node -> bookContracts.add(node.asText())); + }, (code, error) -> logger.error("Error loading contract data: " + code + " " + error))); + + List takerFees = new ArrayList<>(); + List makerFees = new ArrayList<>(); + AtomicReference takerSequencerFee = new AtomicReference<>(); + String walletAddress = exchangeSpecification.getApiKey(); + if (StringUtils.isNotEmpty(walletAddress)) { + queries.add(new Query("{\"type\":\"fee_rates\", \"sender\": \"" + buildSender(walletAddress, getSubAccountOrDefault()) + "\"}", + feeData -> { + readX18DecimalArray(feeData, "taker_fee_rates_x18", takerFees); + readX18DecimalArray(feeData, "maker_fee_rates_x18", makerFees); + takerSequencerFee.set(readX18Decimal(feeData, "taker_sequencer_fee")); + }, (code, error) -> logger.error("Error loading fees data: " + code + " " + error))); + + } + queries.add(new Query("{\"type\":\"all_products\"}", productData -> { + processProductIncrements(productData.withArray("spot_products"), spotProducts); + processProductIncrements(productData.withArray("perp_products"), perpProducts); + + productInfo = new VertexProductInfo(spotProducts, restApiClient.symbols(), takerFees, makerFees, takerSequencerFee.get()); + + + for (Long productId : productInfo.getProductsIds()) { + if (productId != 0) { + Query marketPricesQuery = new Query("{\"type\":\"market_price\", \"product_id\": " + productId + "}", + priceData -> { + JsonNode bidX18 = priceData.get("bid_x18"); + BigInteger bid = new BigInteger(bidX18.asText()); + JsonNode offerX18 = priceData.get("ask_x18"); + BigInteger offer = new BigInteger(offerX18.asText()); + marketPrices.computeIfAbsent(productId, k -> new TopOfBookPrice(convertToDecimal(bid), convertToDecimal(offer))); + }, (code, error) -> logger.error("Error loading market prices: " + code + " " + error)); + priceQueries.add(marketPricesQuery); + } + } + }, (code, error) -> logger.error("Error loading product info: " + code + " " + error))); + + submitQueries(queries.toArray(new Query[0])); + + submitQueries(priceQueries.toArray(new Query[0])); + + } + + private void processProductIncrements(ArrayNode spotProducts, Set productSet) { + for (JsonNode spotProduct : spotProducts) { + long productId = spotProduct.get("product_id").asLong(); + if (productId == 0) { // skip USDC product + continue; + } + productSet.add(productId); + JsonNode bookInfo = spotProduct.get("book_info"); + BigDecimal quantityIncrement = convertToDecimal(new BigInteger(bookInfo.get("size_increment").asText())); + BigDecimal priceIncrement = convertToDecimal(new BigInteger(bookInfo.get("price_increment_x18").asText())); + increments.put(productId, new InstrumentDefinition(priceIncrement, quantityIncrement)); + } + } + + public RewardsList queryRewards(String walletAddress) { + return restApiClient.rewards(new RewardsRequest(new RewardsRequest.RewardAddress(walletAddress))); + } + + public synchronized void submitQueries(Query... queries) { + + Observable stream = subscribeToAllMessages(); + + for (Query query : queries) { + CountDownLatch responseLatch = new CountDownLatch(1); + Disposable subscription = stream.subscribe(resp -> { + JsonNode requestType = resp.get("request_type"); + if (requestType != null && requestType.textValue().startsWith("query_")) { + try { + JsonNode data = resp.get("data"); + JsonNode error = resp.get("error"); + JsonNode errorCode = resp.get("error_code"); + JsonNode status = resp.get("status"); + boolean success = status != null && status.asText().equals("success"); + + if (!success) { + query.getErrorHandler().accept(errorCode.asInt(-1), error.asText("Unknown error")); + } else { + query.getRespHandler().accept(data); + logger.info("Query response " + data.toPrettyString()); + } + } catch (Throwable t) { + logger.error("Query error running " + query.getQueryMsg(), t); + } finally { + responseLatch.countDown(); + } + + } + }, (err) -> logger.error("Query error running " + query.getQueryMsg(), err)); + try { + logger.info("Sending query " + query.getQueryMsg()); + requestResponseStream.sendMessage(query.getQueryMsg()); + if (!responseLatch.await(20, TimeUnit.SECONDS)) { + query.getErrorHandler().accept(-1, "Timed out after 20 seconds waiting for response for " + query.getQueryMsg()); + } + } catch (InterruptedException e) { + logger.error("Failed to get contract data due to timeout"); + } finally { + subscription.dispose(); + } + } + + + } + + public Observable subscribeToAllMessages() { + if (allMessages == null) { + allMessages = requestResponseStream.subscribeChannel(ALL_MESSAGES); + } + return allMessages; + } + + @Override + protected void initServices() { + + String wallet = exchangeSpecification.getApiKey(); + + this.subscriptionStream = createStreamingService("/subscribe"); + this.requestResponseStream = createStreamingService("/ws"); + + this.restApiClient = ExchangeRestProxyBuilder.forInterface(VertexApi.class, exchangeSpecification).build(); + + } + + + @Override + public StreamingMarketDataService getStreamingMarketDataService() { + if (this.streamingMarketDataService == null) { + this.streamingMarketDataService = new VertexStreamingMarketDataService(subscriptionStream, productInfo, this); + } + return streamingMarketDataService; + } + + @Override + public VertexStreamingTradeService getStreamingTradeService() { + if (this.streamingTradeService == null) { + this.streamingTradeService = new VertexStreamingTradeService(requestResponseStream, subscriptionStream, getExchangeSpecification(), productInfo, chainId, bookContracts, this, endpointContract, getStreamingMarketDataService()); + } + return streamingTradeService; + } + + @Override + public TradeService getTradeService() { + return streamingTradeService; + } + + @Override + public Observable connectionStateObservable() { + return subscriptionStream.subscribeConnectionState(); + } + + @Override + public Observable reconnectFailure() { + return subscriptionStream.subscribeReconnectFailure(); + } + + @Override + public Observable connectionSuccess() { + return subscriptionStream.subscribeConnectionSuccess(); + } + + @Override + public Completable connect(ProductSubscription... args) { + if (requestResponseStream.isSocketOpen()) { + return subscriptionStream.connect(); + } + return Completable.concatArray(subscriptionStream.connect(), requestResponseStream.connect()); + } + + @Override + public Completable disconnect() { + return Completable.concatArray(subscriptionStream.disconnect(), requestResponseStream.disconnect()); + } + + @Override + public boolean isAlive() { + return subscriptionStream.isSocketOpen() && requestResponseStream.isSocketOpen(); + } + + @Override + public void useCompressedMessages(boolean compressedMessages) { + subscriptionStream.useCompressedMessages(compressedMessages); + } + + public TopOfBookPrice getMarketPrice(Long productId) { + return marketPrices.get(productId); + } + + public void setMarketPrice(Long productId, TopOfBookPrice price) { + marketPrices.put(productId, price); + } + + + /** + * size and price increments for a product + */ + public InstrumentDefinition getIncrements(Long productId) { + return increments.get(productId); + } + + public VertexApi getRestClient() { + return restApiClient; + } + + /* + Fee charged on taker trades in BPS (always positive) + */ + public BigDecimal getTakerTradeFee(long productId) { + return productInfo.takerTradeFee(productId); + } + + /* + Rebate paid on maker trades in BPS (always positive) + */ + public BigDecimal getMakerTradeFee(long productId) { + return productInfo.makerTradeFee(productId); + } + + String getSubAccountOrDefault() { + return MoreObjects.firstNonNull(exchangeSpecification.getUserName(), DEFAULT_SUB_ACCOUNT); + } + + /* + Fixed USDC fee charged on every taker trade + */ + public BigDecimal getTakerFee() { + return productInfo.takerSequencerFee(); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingMarketDataService.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingMarketDataService.java new file mode 100644 index 00000000000..89c1a08e626 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingMarketDataService.java @@ -0,0 +1,300 @@ +package com.knowm.xchange.vertex; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.knowm.xchange.vertex.dto.PriceAndQuantity; +import com.knowm.xchange.vertex.dto.VertexBestBidOfferMessage; +import com.knowm.xchange.vertex.dto.VertexMarketDataUpdateMessage; +import com.knowm.xchange.vertex.dto.VertexModelUtils; +import com.knowm.xchange.vertex.dto.VertexOrderBookStream; +import com.knowm.xchange.vertex.dto.VertexTradeData; +import info.bitrich.xchangestream.core.StreamingMarketDataService; +import info.bitrich.xchangestream.service.netty.StreamingObjectMapperHelper; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.Subject; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.OrderBook; +import org.knowm.xchange.dto.marketdata.Ticker; +import org.knowm.xchange.dto.marketdata.Trade; +import org.knowm.xchange.instrument.Instrument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class VertexStreamingMarketDataService implements StreamingMarketDataService { + + private static final Logger logger = LoggerFactory.getLogger(VertexStreamingMarketDataService.class); + public static final int DEFAULT_DEPTH = 20; + public static final TypeReference> PRICE_LIST_TYPE_REF = new TypeReference<>() { + }; + + private final VertexStreamingService subscriptionStream; + + private final Map orderBooksStreams = new ConcurrentHashMap<>(); + private final Map> tickerStreams = new ConcurrentHashMap<>(); + + private final Map> tradeSubscriptions = new ConcurrentHashMap<>(); + + private final ObjectMapper mapper; + + private final VertexProductInfo productInfo; + private final VertexStreamingExchange exchange; + private final JavaType PRICE_LIST_TYPE; + + + public VertexStreamingMarketDataService(VertexStreamingService subscriptionStream, VertexProductInfo productInfo, VertexStreamingExchange vertexStreamingExchange) { + this.subscriptionStream = subscriptionStream; + this.productInfo = productInfo; + this.exchange = vertexStreamingExchange; + mapper = StreamingObjectMapperHelper.getObjectMapper(); + mapper.enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + mapper.registerModule(new JavaTimeModule()); + PRICE_LIST_TYPE = mapper.constructType(PRICE_LIST_TYPE_REF.getType()); + } + + @Override + public Observable getTicker(Instrument instrument, Object... args) { + Observable cachedStream = tickerStreams.computeIfAbsent( + instrument, + newInstrument -> { + + logger.info("Subscribing to ticker for " + newInstrument); + + long productId = productInfo.lookupProductId(newInstrument); + String channelName = "best_bid_offer." + productId; + + return subscriptionStream.subscribeChannel(channelName) + .map(jsonNode -> { + VertexBestBidOfferMessage vertexBestBidOfferMessage = mapper.treeToValue(jsonNode, VertexBestBidOfferMessage.class); + BigDecimal bid = VertexModelUtils.convertToDecimal(vertexBestBidOfferMessage.getBid_price()); + BigDecimal ask = VertexModelUtils.convertToDecimal(vertexBestBidOfferMessage.getAsk_price()); + BigDecimal bidQty = VertexModelUtils.convertToDecimal(vertexBestBidOfferMessage.getBid_qty()); + BigDecimal askQty = VertexModelUtils.convertToDecimal(vertexBestBidOfferMessage.getAsk_qty()); + + exchange.setMarketPrice(productId, new TopOfBookPrice(bid, ask)); + + return new Ticker.Builder() + .instrument(newInstrument) + .bid(bid) + .ask(ask) + .bidSize(bidQty) + .askSize(askQty) + .timestamp(new Date(vertexBestBidOfferMessage.getTimestamp().toEpochMilli())) + .creationTimestamp(new Date(Instant.now().toEpochMilli())) + .build(); + + + }).doOnDispose(() -> { + logger.info("Unsubscribing from ticker for " + newInstrument); + tickerStreams.remove(newInstrument); + }); + }); + + return cachedStream.share(); + } + + @Override + public Observable getOrderBook(Instrument instrument, Object... args) { + final int maxDepth; + if (args.length > 0 && args[0] instanceof Integer) { + maxDepth = (int) args[0]; + } else { + maxDepth = DEFAULT_DEPTH; + } + + long productId = productInfo.lookupProductId(instrument); + + StreamHolder cachedInstrumentStream = orderBooksStreams.computeIfAbsent( + instrument, + newInstrument -> { + logger.info("Subscribing to orderBook for " + newInstrument); + + String channelName = "book_depth." + productInfo.lookupProductId(newInstrument); + + AtomicReference snapshotTimeHolder = new AtomicReference<>(); + + Subject snapshots = PublishSubject.create().toSerialized(); + + Observable clearOnDisconnect = subscriptionStream.subscribeDisconnect() + .map(o -> { + logger.info("Clearing order books for {} due to disconnect", newInstrument); + return VertexMarketDataUpdateMessage.EMPTY; + }); + + + AtomicReference lastIncrementTimestamp = new AtomicReference<>(null); + + Observable marketDataUpdates = subscriptionStream.subscribeChannel(channelName) + .map(json -> { + VertexMarketDataUpdateMessage marketDataUpdate = mapper.treeToValue(json, VertexMarketDataUpdateMessage.class); + return Objects.requireNonNullElse(marketDataUpdate, VertexMarketDataUpdateMessage.EMPTY); + }).filter(update -> { + if (update.getLastMaxTime() == null) return true; // Pass through snapshots + + //Subscribe to updates but drop until snapshot reply - there is still a chance we could miss a message but not much we can do about that + Instant snapshotTime = snapshotTimeHolder.get(); + if (snapshotTime == null || update.getMaxTime().isAfter(snapshotTime)) { + if (snapshotTime != null) { + snapshotTimeHolder.set(null); + } + return true; + } + return false; + }); + + Observable updatesWithMissedMsgFilter = marketDataUpdates.filter(update -> { + Instant lastIncrementTime = lastIncrementTimestamp.get(); + if (lastIncrementTime != null && update.getLastMaxTime() != null) { + if (!lastIncrementTime.equals(update.getLastMaxTime())) { + if (update.getMaxTime().equals(lastIncrementTime)) { + logger.trace("Skipping update for {} {} == {}. Already processed.", instrument, lastIncrementTime, update.getLastMaxTime()); + return false; + } + logger.error("Unexpected gap in timestamps for {} {} != {}. Re-snapshot...", instrument, lastIncrementTime, update.getLastMaxTime()); + requestSnapshot(productId, snapshotTimeHolder, snapshots, lastIncrementTimestamp); + return false; + } + } + lastIncrementTimestamp.set(update.getMaxTime()); + + return true; + }); + + Consumer triggerSnapshot = (d) -> requestSnapshot(productId, snapshotTimeHolder, snapshots, lastIncrementTimestamp); + Observable stream = Observable.merge( + snapshots, + updatesWithMissedMsgFilter, + clearOnDisconnect + ) + .doOnDispose(() -> logger.info("Subscription to " + newInstrument + " stopped")) + .share(); + return new StreamHolder(stream, triggerSnapshot); + + } + ); + + + VertexOrderBookStream instrumentAndDepthStream = new VertexOrderBookStream(instrument, maxDepth); + + Disposable instrumentStream = cachedInstrumentStream.getStream() + .subscribe(instrumentAndDepthStream); + + return instrumentAndDepthStream + .doOnSubscribe((d) -> { + //trigger snapshot after delay + Observable.timer(500, TimeUnit.MILLISECONDS) + .subscribe(o -> cachedInstrumentStream.getTriggerSnapshot().accept(d)); + }) + .doOnDispose(instrumentStream::dispose); + + } + + private void requestSnapshot(long productId, AtomicReference snapshotTimeHolder, Subject snapshots, AtomicReference lastChangeId) { + if (Instant.MAX.equals(snapshotTimeHolder.get())) return; // Already requested (or in progress + snapshotTimeHolder.set(Instant.MAX); // Block all updates while we request a snapshot + snapshots.onNext(VertexMarketDataUpdateMessage.EMPTY); //Clear book + // Request snapshot for new subscriber + logger.info("Requesting snapshot for product " + productId); + exchange.submitQueries(new Query(snapshotQuery(DEFAULT_DEPTH, productId), (data) -> { + VertexMarketDataUpdateMessage snapshot = buildSnapshotFromQueryResponse(productId, data); + logger.info("Snapshot for product " + productId + " " + snapshot); + snapshots.onNext(snapshot); + snapshotTimeHolder.set(snapshot.getMaxTime()); + lastChangeId.set(null); + }, (code, error) -> logger.error("Error requesting snapshot for product " + productId + " " + code + " " + error))); + } + + private VertexMarketDataUpdateMessage buildSnapshotFromQueryResponse(long productId, JsonNode data) { + return new VertexMarketDataUpdateMessage(parsePrices(data, "bids"), parsePrices(data, "asks"), NanoSecondsDeserializer.parse(data.get("timestamp").asText()), NanoSecondsDeserializer.parse(data.get("timestamp").asText()), null, productId); + } + + private static String snapshotQuery(int maxDepth, long productId) { + return "{\"type\":\"market_liquidity\",\"product_id\": " + productId + ", \"depth\": " + maxDepth + "}"; + } + + private List parsePrices(JsonNode data, String field) { + ArrayNode bidArray = data.withArray(field); + + List bids; + try { + bids = mapper.treeToValue(bidArray, PRICE_LIST_TYPE); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return bids; + } + + @Override + public Observable getTrades(CurrencyPair instrument, Object... args) { + //noinspection UnnecessaryLocalVariable + Instrument inst = instrument; + return getTrades(inst, args); + } + + @Override + public Observable getTrades(Instrument instrument, Object... args) { + + return tradeSubscriptions.computeIfAbsent( + instrument, + newInstrument -> { + + String channelName = "trade." + productInfo.lookupProductId(instrument); + + logger.info("Subscribing to trade channel: " + channelName); + + return subscriptionStream.subscribeChannel(channelName) + .map(json -> mapper.treeToValue(json, VertexTradeData.class).toTrade(instrument)); + + + }).share(); + } + + + private static class StreamHolder { + private final Observable stream; + private final Consumer triggerSnapshot; + + public StreamHolder(Observable stream, Consumer triggerSnapshot) { + this.stream = stream; + this.triggerSnapshot = triggerSnapshot; + } + + public Observable getStream() { + return stream; + } + + public Consumer getTriggerSnapshot() { + return triggerSnapshot; + } + } + + + @Override + public Observable getTicker(CurrencyPair currencyPair, Object... args) { + return this.getTicker((Instrument) currencyPair, args); + } + + @Override + public Observable getOrderBook(CurrencyPair currencyPair, Object... args) { + return this.getOrderBook((Instrument) currencyPair, args); + } + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingService.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingService.java new file mode 100644 index 00000000000..627187977d9 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingService.java @@ -0,0 +1,104 @@ +package com.knowm.xchange.vertex; + +import com.fasterxml.jackson.databind.JsonNode; +import info.bitrich.xchangestream.service.netty.JsonNettyStreamingService; +import io.reactivex.Completable; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VertexStreamingService extends JsonNettyStreamingService { + + private static final Logger logger = LoggerFactory.getLogger(VertexStreamingService.class); + + //Channel to use to subscribe to all response + public static final String ALL_MESSAGES = "all_messages"; + + private final AtomicLong reqCounter = new AtomicLong(1); + private final String apiUrl; + + public VertexStreamingService(String apiUrl) { + super(apiUrl); + this.apiUrl = apiUrl; + } + + @Override + protected String getChannelNameFromMessage(JsonNode message) { + JsonNode type = message.get("type"); + JsonNode productId = message.get("product_id"); + JsonNode subaccount = message.get("subaccount"); + if (type != null) { + if (productId != null) { + if (subaccount != null) { + return type.asText() + "." + productId.asText() + "." + subaccount.asText(); + } + return type.asText() + "." + productId.asText(); + } + return type.asText(); + } else { + return ALL_MESSAGES; + } + + } + + @Override + public String getSubscribeMessage(String channelName, Object... args) { + if (Objects.equals(channelName, ALL_MESSAGES)) { + return null; + } + String[] typeAndProduct = channelName.split("\\."); + long reqId = reqCounter.incrementAndGet(); + return "{\n" + + " \"method\": \"subscribe\",\n" + + " \"stream\": {\n" + + " \"type\": \"" + typeAndProduct[0] + "\"\n" + + productIdField(typeAndProduct) + + subAccountField(typeAndProduct) + + " },\n" + + " \"id\": " + reqId + "\n" + + "}"; + } + + private static String productIdField(String[] typeAndProduct) { + return typeAndProduct.length > 1 ? ", \"product_id\": " + typeAndProduct[1] + "\n" : ""; + } + + private String subAccountField(String[] typeAndProduct) { + if (typeAndProduct.length > 2) { + return ",\"subaccount\": \"" + typeAndProduct[2] + "\"\n"; + } else { + return ""; + } + } + + @Override + public String getUnsubscribeMessage(String channelName, Object... args) { + if (Objects.equals(channelName, ALL_MESSAGES)) { + return null; + } + String[] typeAndProduct = channelName.split("\\."); + long reqId = reqCounter.incrementAndGet(); + return "{\n" + + " \"method\": \"unsubscribe\",\n" + + " \"stream\": {\n" + + " \"type\": \"" + typeAndProduct[0] + "\"\n" + + productIdField(typeAndProduct) + + subAccountField(typeAndProduct) + + " },\n" + + " \"id\": " + reqId + "\n" + + "}"; + } + + + @Override + public Completable disconnect() { + if (isSocketOpen()) { + logger.info("Disconnecting " + apiUrl); + return super.disconnect(); + } else { + logger.info("Already disconnected " + apiUrl); + return Completable.complete(); + } + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingTradeService.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingTradeService.java new file mode 100644 index 00000000000..1b9570b18e7 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/VertexStreamingTradeService.java @@ -0,0 +1,764 @@ +package com.knowm.xchange.vertex; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import static com.knowm.xchange.vertex.VertexStreamingExchange.DEFAULT_SUB_ACCOUNT; +import static com.knowm.xchange.vertex.VertexStreamingExchange.MAX_SLIPPAGE_RATIO; +import static com.knowm.xchange.vertex.VertexStreamingExchange.PLACE_ORDER_VALID_UNTIL_MS_PROP; +import static com.knowm.xchange.vertex.VertexStreamingExchange.USE_LEVERAGE; +import com.knowm.xchange.vertex.dto.CancelOrders; +import com.knowm.xchange.vertex.dto.CancelProductOrders; +import com.knowm.xchange.vertex.dto.Tx; +import com.knowm.xchange.vertex.dto.VertexCancelOrdersMessage; +import com.knowm.xchange.vertex.dto.VertexCancelProductOrdersMessage; +import com.knowm.xchange.vertex.dto.VertexModelUtils; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.buildNonce; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.buildSender; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.convertToInteger; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.readX18Decimal; +import com.knowm.xchange.vertex.dto.VertexOrder; +import com.knowm.xchange.vertex.dto.VertexPlaceOrder; +import com.knowm.xchange.vertex.dto.VertexPlaceOrderMessage; +import com.knowm.xchange.vertex.dto.VertexRequest; +import com.knowm.xchange.vertex.signing.MessageSigner; +import com.knowm.xchange.vertex.signing.SignatureAndDigest; +import com.knowm.xchange.vertex.signing.schemas.CancelOrdersSchema; +import com.knowm.xchange.vertex.signing.schemas.CancelProductOrdersSchema; +import com.knowm.xchange.vertex.signing.schemas.PlaceOrderSchema; +import info.bitrich.xchangestream.core.StreamingMarketDataService; +import info.bitrich.xchangestream.core.StreamingTradeService; +import info.bitrich.xchangestream.service.netty.ConnectionStateModel; +import info.bitrich.xchangestream.service.netty.StreamingObjectMapperHelper; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.account.OpenPosition; +import org.knowm.xchange.dto.account.OpenPositions; +import org.knowm.xchange.dto.marketdata.Ticker; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.dto.trade.MarketOrder; +import org.knowm.xchange.dto.trade.OpenOrders; +import org.knowm.xchange.dto.trade.UserTrade; +import org.knowm.xchange.exceptions.ExchangeException; +import org.knowm.xchange.instrument.Instrument; +import org.knowm.xchange.service.trade.TradeService; +import org.knowm.xchange.service.trade.params.CancelAllOrders; +import org.knowm.xchange.service.trade.params.CancelOrderByCurrencyPair; +import org.knowm.xchange.service.trade.params.CancelOrderByIdParams; +import org.knowm.xchange.service.trade.params.CancelOrderByInstrument; +import org.knowm.xchange.service.trade.params.CancelOrderParams; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParamInstrument; +import org.knowm.xchange.service.trade.params.orders.OpenOrdersParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VertexStreamingTradeService implements StreamingTradeService, TradeService { + + public static final double DEFAULT_MAX_SLIPPAGE_RATIO = 0.005; + public static final ObjectMapper MAPPER = new ObjectMapper(); + private static final boolean DEFAULT_USE_LEVERAGE = false; + public static final Consumer NO_OP = ticker -> { + }; + public static final HashFunction ORDER_ID_HASHER = Hashing.murmur3_32_fixed(); + public static final BigDecimal BPS_TO_MULTIPLIER = BigDecimal.valueOf(0.0001); + private final Logger logger = LoggerFactory.getLogger(VertexStreamingTradeService.class); + + private final VertexStreamingService requestResponseStream; + private final VertexStreamingService subscriptionStream; + private final ExchangeSpecification exchangeSpecification; + private final ObjectMapper mapper; + + private final VertexProductInfo productInfo; + private final long chainId; + private final List bookContracts; + private final VertexStreamingExchange exchange; + private final String endpointContract; + private final double slippage; + private final boolean useLeverage; + private final int placeOrderValidUntilMs; + private final Map, CompletableFuture> responses = new ConcurrentHashMap<>(); + private final Map tickerSubscriptions = new ConcurrentHashMap<>(); + private final Map> fillSubscriptions = new ConcurrentHashMap<>(); + private final Disposable allMessageSubscription; + private final StreamingMarketDataService marketDataService; + + public VertexStreamingTradeService(VertexStreamingService requestResponseStream, VertexStreamingService subscriptionStream, ExchangeSpecification exchangeSpecification, VertexProductInfo productInfo, long chainId, List bookContracts, VertexStreamingExchange exchange, String endpointContract, StreamingMarketDataService marketDataService) { + this.requestResponseStream = requestResponseStream; + this.subscriptionStream = subscriptionStream; + this.exchangeSpecification = exchangeSpecification; + this.productInfo = productInfo; + this.chainId = chainId; + this.bookContracts = bookContracts; + this.endpointContract = endpointContract; + this.exchange = exchange; + this.marketDataService = marketDataService; + this.mapper = StreamingObjectMapperHelper.getObjectMapper(); + this.slippage = exchangeSpecification.getExchangeSpecificParametersItem(MAX_SLIPPAGE_RATIO) != null ? Double.parseDouble(exchangeSpecification.getExchangeSpecificParametersItem(MAX_SLIPPAGE_RATIO).toString()) : DEFAULT_MAX_SLIPPAGE_RATIO; + this.useLeverage = exchangeSpecification.getExchangeSpecificParametersItem(USE_LEVERAGE) != null ? Boolean.parseBoolean(exchangeSpecification.getExchangeSpecificParametersItem(USE_LEVERAGE).toString()) : DEFAULT_USE_LEVERAGE; + this.placeOrderValidUntilMs = exchangeSpecification.getExchangeSpecificParametersItem(PLACE_ORDER_VALID_UNTIL_MS_PROP) != null ? (int) exchangeSpecification.getExchangeSpecificParametersItem(PLACE_ORDER_VALID_UNTIL_MS_PROP) : 60000; + + exchange.connectionStateObservable().subscribe( + s -> { + if (!ConnectionStateModel.State.CLOSED.equals(s)) { + return; + } + + Collection> futures = responses.values(); + + if (futures.isEmpty()) { + return; + } + + logger.info("Cancelling {} pending operations due to {} state", futures.size(), s); + + futures.forEach(f -> f.cancel(false)); + responses.clear(); + }, + t -> logger.error("Connection state observer error", t) + ); + + this.allMessageSubscription = exchange.subscribeToAllMessages().subscribe(resp -> { + JsonNode typeNode = resp.get("request_type"); + + if (typeNode != null && typeNode.textValue().startsWith("query")) { + return; // ignore query responses that are handled in VertexStreamingExchange + } + + JsonNode statusNode = resp.get("status"); + JsonNode signatureNode = resp.get("signature"); + + if (statusNode == null || typeNode == null || signatureNode == null) { + logger.error("Unable to handle incomplete response: {}", resp); + return; + } + + boolean success = "success".equals(statusNode.asText()); + String type = typeNode.asText(); + String signature = signatureNode.asText(); + + CompletableFuture responseFuture = responses.remove(Pair.of(type, signature)); + + if (responseFuture != null) { + if (success) { + logger.info("Received success for {} ({}): {}", type, signature, resp); + responseFuture.complete(resp); + } else { + logger.error("Received error for {} ({}): {}", type, signature, resp); + responseFuture.completeExceptionally(new ExchangeException(resp.get("error").asText())); + } + + } else { + if (success) { + logger.warn("Received success for unknown {} ({}): {}", type, signature, resp); + } else { + logger.error("Received error for unknown {} ({}): {}", type, signature, resp); + } + + } + }); + } + + public void disconnect() { + allMessageSubscription.dispose(); + tickerSubscriptions.values().stream().filter(Disposable::isDisposed).forEach(Disposable::dispose); + if (requestResponseStream.isSocketOpen()) { + if (!requestResponseStream.disconnect().blockingAwait(10, TimeUnit.SECONDS)) { + logger.warn("Timeout waiting for disconnect"); + } + } + } + + @Override + public String placeLimitOrder(LimitOrder limitOrder) { + BigDecimal price = limitOrder.getLimitPrice(); + + return placeOrder(limitOrder, price); + } + + @Override + public String placeMarketOrder(MarketOrder marketOrder) { + long productId = productInfo.lookupProductId(marketOrder.getInstrument()); + + BigDecimal price = getPrice(marketOrder, productId); + + return placeOrder(marketOrder, price); + } + + @Override + public Observable getOrderChanges(Instrument instrument, Object... args) { + return subscribeToFills(instrument).map(resp -> { + boolean isBid = resp.get("is_bid").asBoolean(); + Order.Builder builder = new LimitOrder.Builder(isBid ? Order.OrderType.BID : Order.OrderType.ASK, instrument); + String orderId = resp.get("order_digest").asText(); + BigDecimal original = readX18Decimal(resp, "original_qty"); + BigDecimal remaining = readX18Decimal(resp, "remaining_qty"); + BigDecimal price = readX18Decimal(resp, "price"); + BigDecimal filled = readX18Decimal(resp, "filled_qty"); + Instant timestamp = NanoSecondsDeserializer.parse(resp.get("timestamp").asText()); + String respSubAccount = resp.get("subaccount").asText(); + Order.OrderStatus status = getOrderStatus(remaining, filled, original); + return builder.id(orderId) + .instrument(instrument) + .originalAmount(original) + .cumulativeAmount(filled) + .orderStatus(status) + .averagePrice(price) + .remainingAmount(remaining) + .userReference(respSubAccount) + .timestamp(new Date(timestamp.toEpochMilli())) + .build(); + }); + } + + private static Order.OrderStatus getOrderStatus(BigDecimal remaining, BigDecimal filled, BigDecimal original) { + Order.OrderStatus status; + if (isZero(remaining) || filled.equals(original)) { + status = Order.OrderStatus.FILLED; + } else if (isZero(filled) || remaining.equals(original)) { + status = Order.OrderStatus.NEW; + } else { + status = Order.OrderStatus.PARTIALLY_FILLED; + } + return status; + } + + private static boolean isZero(BigDecimal remaining) { + return remaining.compareTo(BigDecimal.ZERO) == 0; + } + + private Observable subscribeToFills(Instrument instrument) { + long productId = productInfo.lookupProductId(instrument); + + String subAccount = exchange.getSubAccountOrDefault(); + + String channel = "fill." + productId + "." + buildSender(exchangeSpecification.getApiKey(), subAccount); + return fillSubscriptions.computeIfAbsent(channel, c -> subscriptionStream.subscribeChannel(channel)); + } + + @Override + public Observable getUserTrades(Instrument instrument, Object... args) { + long productId = productInfo.lookupProductId(instrument); + + return subscribeToFills(instrument).map(resp -> { + boolean isBid = resp.get("is_bid").asBoolean(); + boolean isTaker = resp.get("is_taker").asBoolean(); + UserTrade.Builder builder = new UserTrade.Builder(); + + String orderId = resp.get("order_digest").asText(); + BigDecimal price = readX18Decimal(resp, "price"); + BigDecimal filled = readX18Decimal(resp, "filled_qty"); + + if (isZero(filled)) { + return Optional.empty(); + } + String timestampText = resp.get("timestamp").asText(); + Instant timestamp = NanoSecondsDeserializer.parse(timestampText); + String respSubAccount = resp.get("subaccount").asText(); + BigDecimal orderQty = readX18Decimal(resp, "original_qty"); + BigDecimal remaining = readX18Decimal(resp, "remaining_qty"); + BigDecimal totalFilled = orderQty.subtract(remaining); + String filledPercentage = totalFilled.divide(orderQty, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)).setScale(3, RoundingMode.HALF_DOWN).toPlainString(); + boolean isFirstFill = totalFilled.compareTo(filled) == 0; + + BigDecimal fee = calcFee(isTaker, filled, productId, price, isFirstFill); + return Optional.of(builder.id(ORDER_ID_HASHER.hashString(orderId + ":" + totalFilled.toPlainString() + ":" + price.toPlainString(), Charsets.UTF_8) + "-" + filledPercentage) + .instrument(instrument) + .originalAmount(filled) + .orderId(orderId) + .price(price) + .type(isBid ? Order.OrderType.BID : Order.OrderType.ASK) + .orderUserReference(respSubAccount) + .timestamp(new Date(timestamp.toEpochMilli())) + .feeCurrency(Currency.USDC) + .feeAmount(fee) + .creationTimestamp(new Date()) + .build()); + }) + .filter(Optional::isPresent) + .map(Optional::get); + } + + private BigDecimal calcFee(boolean isTaker, BigDecimal filled, long productId, BigDecimal price, boolean isFirstFill) { + BigDecimal bpsFee = isTaker ? exchange.getTakerTradeFee(productId) : exchange.getMakerTradeFee(productId); + BigDecimal lhsFee = filled.multiply(bpsFee); + + //Fixed sequencer fee is only charged on first fill per order + BigDecimal usdcFee = lhsFee.multiply(price).setScale(2, RoundingMode.HALF_UP); + if (isTaker && isFirstFill) { + usdcFee = usdcFee.add(exchange.getTakerFee()); + } + return isTaker ? usdcFee : usdcFee.negate(); + } + + public OpenPositions getOpenPositions() throws IOException { + CountDownLatch response = new CountDownLatch(1); + + String subAccount = exchange.getSubAccountOrDefault(); + AtomicReference subAccountInfoHolder = new AtomicReference<>(); + exchange.submitQueries(new Query(subAccountInfo(subAccount), newValue -> { + subAccountInfoHolder.set(newValue); + response.countDown(); + }, (code, error) -> { + logger.error("Error getting subaccount info: {} {}", code, error); + response.countDown(); + })); + + try { + if (!response.await(10, TimeUnit.SECONDS)) { + throw new IOException("Timeout waiting for open positions response"); + } + + + JsonNode summary = exchange.getRestClient().indexerRequest(summary(subAccount)); + + JsonNode subAccountInfo = subAccountInfoHolder.get(); + List positions = new ArrayList<>(); + subAccountInfo.withArray("spot_balances").elements().forEachRemaining(bal -> addBalance(positions, bal, summary)); + subAccountInfo.withArray("perp_balances").elements().forEachRemaining(bal -> addBalance(positions, bal, summary)); + + return new OpenPositions(positions); + } catch (InterruptedException ignored) { + return new OpenPositions(Collections.emptyList()); + } + + + } + + private JsonNode summary(String subAccount) { + String sender = buildSender(exchangeSpecification.getApiKey(), subAccount); + String jsonString = String.format("{\"summary\": {\"subaccount\": \"%s\"}}", sender); + + try { + return MAPPER.readTree(jsonString); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private void addBalance(List positions, JsonNode bal, JsonNode summary) { + int productId = bal.get("product_id").asInt(); + Instrument instrument = productInfo.lookupInstrument(productId); + if (instrument == null) { + logger.warn("No instrument found for product id {}", productId); + return; + } + BigDecimal position = readX18Decimal(bal.get("balance"), "amount"); + if (isZero(position)) { + return; + } + BigDecimal price = findPrice(productId, summary); + positions.add(new OpenPosition(instrument, position.compareTo(BigDecimal.ZERO) >= 0 ? OpenPosition.Type.LONG : OpenPosition.Type.SHORT, position.abs(), price, null, null)); + } + + private BigDecimal findPrice(int productId, JsonNode summary) { + Iterator events = summary.withArray("events").elements(); + + while (events.hasNext()) { + JsonNode event = events.next(); + if (event.get("product_id").asInt() == productId) { + JsonNode postBalance = event.get("post_balance"); + BigDecimal balance = readX18Decimal(MoreObjects.firstNonNull(postBalance.get("perp"), postBalance.get("spot")).get("balance"), "amount"); + BigDecimal netUnrealised = readX18Decimal(event, "net_entry_unrealized"); + return netUnrealised.divide(balance, RoundingMode.HALF_UP).abs(); + } + } + return null; + } + + private String subAccountInfo(String subAccount) { + String sender = buildSender(exchangeSpecification.getApiKey(), subAccount); + return String.format("{\"type\": \"subaccount_info\",\"subaccount\": \"%s\"}", sender); + } + + private String placeOrder(Order marketOrder, BigDecimal price) { + Instrument instrument = marketOrder.getInstrument(); + long productId = productInfo.lookupProductId(instrument); + + BigInteger expiration = getExpiration(marketOrder.getOrderFlags()); + + InstrumentDefinition increments = exchange.getIncrements(productId); + BigDecimal priceIncrement = increments.getPriceIncrement(); + price = roundToIncrement(price, priceIncrement); + BigInteger priceAsInt = convertToInteger(price); + + BigDecimal quantity = getQuantity(marketOrder); + BigDecimal quantityIncrement = increments.getQuantityIncrement(); + if (quantity.abs().compareTo(quantityIncrement) < 0) { + throw new IllegalArgumentException("Quantity must be greater than increment"); + } + quantity = roundToIncrement(quantity, quantityIncrement); + BigInteger quantityAsInt = convertToInteger(quantity); + + String subAccount = exchange.getSubAccountOrDefault(); + String nonce = buildNonce(placeOrderValidUntilMs); + String walletAddress = exchangeSpecification.getApiKey(); + String sender = VertexModelUtils.buildSender(walletAddress, subAccount); + + String bookContract = bookContracts.get((int) productId); + PlaceOrderSchema orderSchema = PlaceOrderSchema.build(chainId, + bookContract, + Long.valueOf(nonce), + sender, + expiration, + quantityAsInt, + priceAsInt); + SignatureAndDigest signatureAndDigest = new MessageSigner(exchangeSpecification.getSecretKey()).signMessage(orderSchema); + + VertexPlaceOrderMessage orderMessage = new VertexPlaceOrderMessage(new VertexPlaceOrder( + productId, + new VertexOrder(sender, priceAsInt.toString(), quantityAsInt.toString(), expiration.toString(), nonce), + signatureAndDigest.getSignature(), + productInfo.isSpot(instrument) ? useLeverage : null)); + + logger.info("Send order {} -> {} (valid for {}ms)", marketOrder, signatureAndDigest, placeOrderValidUntilMs); + + try { + sendWebsocketMessage(orderMessage); + + } catch (Throwable e) { + logger.error("Failed to place order : " + orderMessage, e); + throw new ExchangeException(e); + + } + + return signatureAndDigest.getDigest(); + } + + private JsonNode sendWebsocketMessage(VertexRequest messageObj) throws ExecutionException, InterruptedException, TimeoutException, JsonProcessingException { + String requestType = messageObj.getRequestType(); + String signature = messageObj.getSignature(); + + String message = mapper.writeValueAsString(messageObj); + + logger.info("Sending {} ({}): {}", requestType, signature, message); + + CompletableFuture responseFuture = getResponseFuture(requestType, signature); + + requestResponseStream.sendMessage(message); + + try { + return responseFuture.get(5000, TimeUnit.MILLISECONDS); + + } catch (Throwable e) { + responses.remove(Pair.of(requestType, signature)); + throw e; + + } + } + + private CompletableFuture getResponseFuture(String requestType, String signature) { + CompletableFuture responseFuture = new CompletableFuture<>(); + CompletableFuture oldFuture = responses.putIfAbsent(Pair.of(requestType, signature), responseFuture); + Preconditions.checkState(oldFuture == null, "Already pending a response for %s (%s): %s", requestType, signature, oldFuture); + return responseFuture; + } + + private BigInteger getExpiration(Set orderFlags) { + BigInteger timeInForce = BigInteger.ZERO; // resting + Instant expiryTime = Instant.MAX; // No expiry + if (orderFlags.contains(VertexOrderFlags.TIME_IN_FORCE_IOC)) { + timeInForce = BigInteger.ONE; + expiryTime = Instant.now().plus(5, ChronoUnit.SECONDS); // Force IOC/FOK timeouts + } else if (orderFlags.contains(VertexOrderFlags.TIME_IN_FORCE_FOK)) { + timeInForce = BigInteger.valueOf(2); + expiryTime = Instant.now().plus(5, ChronoUnit.SECONDS); // Force IOC/FOK timeouts + } else if (orderFlags.contains(VertexOrderFlags.TIME_IN_FORCE_POS_ONLY)) { + timeInForce = BigInteger.valueOf(3); + } + + BigInteger expiry = BigInteger.valueOf(expiryTime.getEpochSecond()); + BigInteger tifMask = timeInForce.shiftLeft(62); + return expiry.or(tifMask); + } + + private BigDecimal getPrice(Order order, long productId) { + BigDecimal price; + if (order instanceof LimitOrder) { + price = ((LimitOrder) order).getLimitPrice(); + } else { + // Make sure we have a subscription to the ticker for market prices + tickerSubscriptions.computeIfAbsent(productId, id -> marketDataService.getTicker(order.getInstrument()).forEach(NO_OP)); + TopOfBookPrice bidOffer = exchange.getMarketPrice(productId); + boolean isSell = order.getType().equals(Order.OrderType.ASK); + if (isSell) { + BigDecimal bid = bidOffer.getBid(); + // subtract max slippage from bid + price = bid.subtract(bid.multiply(BigDecimal.valueOf(slippage))); + } else { + BigDecimal offer = bidOffer.getOffer(); + // add max slippage to offer + price = offer.add(offer.multiply(BigDecimal.valueOf(slippage))); + } + } + return price; + } + + @Override + public Collection cancelAllOrders(CancelAllOrders orderParams) { + cancelOrder(orderParams); + return Collections.emptyList(); + } + + @Override + public boolean cancelOrder(CancelOrderParams params) { + + String id = getOrderId(params); + Instrument instrument = getInstrument(params); + + if (StringUtils.isNotEmpty(id) && instrument != null) { + + long productId = productInfo.lookupProductId(instrument); + + String subAccount = exchange.getSubAccountOrDefault(); + String nonce = buildNonce(60000); + String walletAddress = exchangeSpecification.getApiKey(); + String sender = VertexModelUtils.buildSender(walletAddress, subAccount); + long[] productIds = {productId}; + String[] digests = {id}; + + CancelOrdersSchema orderSchema = CancelOrdersSchema.build(chainId, endpointContract, Long.valueOf(nonce), sender, productIds, digests); + SignatureAndDigest signatureAndDigest = new MessageSigner(exchangeSpecification.getSecretKey()).signMessage(orderSchema); + + VertexCancelOrdersMessage orderMessage = new VertexCancelOrdersMessage(new CancelOrders( + new Tx(sender, productIds, digests, nonce), + signatureAndDigest.getSignature() + )); + + try { + sendWebsocketMessage(orderMessage); + return true; + + } catch (Throwable e) { + logger.error("Failed to cancel order (" + id + "): " + orderMessage, e); + return isAlreadyCancelled(Throwables.getRootCause(e)); + + } + + } else if (params instanceof CancelAllOrders || instrument != null) { + List productIds = new ArrayList<>(); + if (instrument != null) { + productIds.add(productInfo.lookupProductId(instrument)); + } + + String subAccount = exchange.getSubAccountOrDefault(); + String nonce = buildNonce(60000); + String walletAddress = exchangeSpecification.getApiKey(); + String sender = VertexModelUtils.buildSender(walletAddress, subAccount); + + long[] productIdsArray = productIds.stream().mapToLong(l -> l).toArray(); + + CancelProductOrdersSchema cancelAllSchema = CancelProductOrdersSchema.build(chainId, endpointContract, Long.valueOf(nonce), sender, productIdsArray); + + SignatureAndDigest signatureAndDigest = new MessageSigner(exchangeSpecification.getSecretKey()).signMessage(cancelAllSchema); + + VertexCancelProductOrdersMessage orderMessage = new VertexCancelProductOrdersMessage(new CancelProductOrders( + new Tx(sender, productIdsArray, null, nonce), + signatureAndDigest.getSignature() + )); + + try { + sendWebsocketMessage(orderMessage); + return true; + + } catch (Throwable e) { + logger.error("Failed to cancel order " + orderMessage, e); + return false; + + } + + } + throw new IllegalArgumentException( + "CancelOrderParams must implement some of CancelOrderByIdParams, CancelOrderByInstrument, CancelOrderByCurrencyPair, CancelAllOrders interfaces."); + } + + private boolean isAlreadyCancelled(Throwable throwable) { + // Treat this as a successful cancel as automatic/unsolicited cancellations are not notified + String message = throwable.getMessage(); + return message != null && message.matches(".*Order with the provided digest .* could not be found.*"); + } + + private String getOrderId(CancelOrderParams params) { + if (params instanceof CancelOrderByIdParams) { + return ((CancelOrderByIdParams) params).getOrderId(); + } + return null; + } + + private Instrument getInstrument(CancelOrderParams params) { + if (params instanceof CancelOrderByCurrencyPair || params instanceof CancelOrderByInstrument) { + return params instanceof CancelOrderByCurrencyPair ? ((CancelOrderByCurrencyPair) params).getCurrencyPair() : ((CancelOrderByInstrument) params).getInstrument(); + } + return null; + } + + private String getSubAccountOrDefault() { + return MoreObjects.firstNonNull(exchangeSpecification.getUserName(), DEFAULT_SUB_ACCOUNT); + } + + @Override + public OpenOrders getOpenOrders() throws IOException { + return getOpenOrders(null); + } + + @Override + public OpenOrders getOpenOrders(OpenOrdersParams params) throws IOException { + try { + + + CompletableFuture responseLatch = new CompletableFuture<>(); + + if (params instanceof OpenOrdersParamInstrument) { + CurrencyPair instrument = (CurrencyPair) ((OpenOrdersParamInstrument) params).getInstrument(); + long productId = productInfo.lookupProductId(instrument); + + String subAccount = exchange.getSubAccountOrDefault(); + exchange.submitQueries(new Query(openOrders(productId, subAccount), (data) -> { + List orders = new ArrayList<>(); + data.withArray("orders").elements().forEachRemaining(order -> { + String priceX18 = "price_x18"; + BigDecimal price = readX18Decimal(order, priceX18); + BigDecimal amount = readX18Decimal(order, "amount"); + BigDecimal unfilledAmount = readX18Decimal(order, "unfilled_amount"); + + Date placedAt = new Date(Instant.ofEpochSecond(order.get("placed_at").asLong()).toEpochMilli()); + BigDecimal filled = amount.subtract(unfilledAmount); + LimitOrder.Builder builder = new LimitOrder.Builder(amount.compareTo(BigDecimal.ZERO) > 0 ? Order.OrderType.BID : Order.OrderType.ASK, instrument) + .id(order.get("digest").asText()) + .limitPrice(price) + .originalAmount(amount) + .remainingAmount(unfilledAmount) + .orderStatus(getOrderStatus(unfilledAmount, filled, amount)) + .cumulativeAmount(filled) + .timestamp(placedAt); + orders.add(builder.build()); + + }); + responseLatch.complete(new OpenOrders(orders)); + }, (code, error) -> responseLatch.completeExceptionally(new ExchangeException("Failed to get open orders: " + error)))); + + return responseLatch.get(10, TimeUnit.SECONDS); + + } else { + + String subAccount = getSubAccountOrDefault(); + List orders = new ArrayList<>(); + + List queries = new ArrayList<>(); + List productsIds = productInfo.getProductsIds().stream().filter(id -> id != 0).collect(Collectors.toList()); + CountDownLatch pendingQueries = new CountDownLatch(productsIds.size()); + for (Long productId : productsIds) { + Instrument instrument = productInfo.lookupInstrument(productId); + queries.add(new Query(openOrders(productId, subAccount), (data) -> { + data.withArray("orders").elements().forEachRemaining(order -> { + String priceX18 = "price_x18"; + BigDecimal price = readX18Decimal(order, priceX18); + BigDecimal amount = readX18Decimal(order, "amount"); + BigDecimal unfilledAmount = readX18Decimal(order, "unfilled_amount"); + + Date placedAt = new Date(Instant.ofEpochSecond(order.get("placed_at").asLong()).toEpochMilli()); + BigDecimal filled = amount.subtract(unfilledAmount); + LimitOrder.Builder builder = new LimitOrder.Builder(amount.compareTo(BigDecimal.ZERO) > 0 ? Order.OrderType.BID : Order.OrderType.ASK, instrument) + .id(order.get("digest").asText()) + .limitPrice(price) + .originalAmount(amount) + .remainingAmount(unfilledAmount) + .orderStatus(getOrderStatus(unfilledAmount, filled, amount)) + .cumulativeAmount(filled) + .timestamp(placedAt); + orders.add(builder.build()); + + }); + pendingQueries.countDown(); + }, (code, error) -> { + pendingQueries.countDown(); + responseLatch.completeExceptionally(new ExchangeException("Failed to get open orders: " + error)); + })); + } + + exchange.submitQueries(queries.toArray(new Query[0])); + if (!pendingQueries.await(10, TimeUnit.SECONDS)) { + throw new IOException("Timeout waiting for open orders response"); + } + responseLatch.complete(new OpenOrders(orders)); + } + + return responseLatch.get(10, TimeUnit.SECONDS); + } catch (InterruptedException | CancellationException ignored) { + return new OpenOrders(Collections.emptyList()); + } catch (TimeoutException e) { + throw new IOException("Timeout waiting for open orders response"); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + } + + private String openOrders(long productId, String subAccount) { + String sender = buildSender(exchangeSpecification.getApiKey(), subAccount); + return String.format("{\"type\": \"subaccount_orders\",\"sender\": \"%s\",\"product_id\": %d}", sender, productId); + } + + private BigDecimal getQuantity(Order order) { + BigDecimal quantityAsInt = order.getOriginalAmount(); + if (order.getType().equals(Order.OrderType.ASK)) { + quantityAsInt = quantityAsInt.multiply(BigDecimal.valueOf(-1)); + } + return quantityAsInt; + } + + public static BigDecimal roundToIncrement(BigDecimal value, BigDecimal increment) { + if (isZero(increment)) return value; + BigDecimal divided = value.divide(increment, 0, RoundingMode.FLOOR); + return divided.multiply(increment); + } + + + @Override + public Observable getOrderChanges(CurrencyPair currencyPair, Object... args) { + return getOrderChanges((Instrument) currencyPair, args); + } + + @Override + public Observable getUserTrades(CurrencyPair currencyPair, Object... args) { + return getUserTrades((Instrument) currencyPair, args); + } + + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/api/VertexApi.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/api/VertexApi.java new file mode 100644 index 00000000000..1f408e6ee73 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/api/VertexApi.java @@ -0,0 +1,35 @@ +package com.knowm.xchange.vertex.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.knowm.xchange.vertex.dto.RewardsList; +import com.knowm.xchange.vertex.dto.RewardsRequest; +import com.knowm.xchange.vertex.dto.Symbol; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + + +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Path("/") +public interface VertexApi { + + + @POST + @Path("/indexer") + RewardsList rewards(RewardsRequest req); + + @POST + @Path("/indexer") + JsonNode indexerRequest(JsonNode req); + + + @GET + @Path("/symbols") + Symbol[] symbols(); +} + + diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/AddressRewards.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/AddressRewards.java new file mode 100644 index 00000000000..c82b484c893 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/AddressRewards.java @@ -0,0 +1,24 @@ +package com.knowm.xchange.vertex.dto; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.ToString; + +@ToString +@Getter +public class AddressRewards { + + private int product_id; + private BigDecimal q_score; + private BigDecimal sum_q_min; + private long uptime; + private BigDecimal maker_volume; + private BigDecimal taker_volume; + private BigDecimal maker_fee; + private BigDecimal taker_fee; + private BigDecimal maker_tokens; + private BigDecimal taker_tokens; + private BigDecimal rebates; + + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/CancelOrders.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/CancelOrders.java new file mode 100644 index 00000000000..3458e92f765 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/CancelOrders.java @@ -0,0 +1,19 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class CancelOrders { + + private final Tx tx; + private final String signature; + + public CancelOrders(@JsonProperty("tx") Tx tx, @JsonProperty("signature") String signature) { + this.tx = tx; + this.signature = signature; + } + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/CancelProductOrders.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/CancelProductOrders.java new file mode 100644 index 00000000000..a6df6f495aa --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/CancelProductOrders.java @@ -0,0 +1,16 @@ +package com.knowm.xchange.vertex.dto; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class CancelProductOrders { + private final Tx tx; + private final String signature; + + public CancelProductOrders(Tx tx, String signature) { + this.tx = tx; + this.signature = signature; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/GlobalRewards.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/GlobalRewards.java new file mode 100644 index 00000000000..43f1afcfc84 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/GlobalRewards.java @@ -0,0 +1,22 @@ +package com.knowm.xchange.vertex.dto; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class GlobalRewards { + + private int product_id; + private long uptimes; + private BigDecimal reward_coefficient; + private BigDecimal q_scores; + private BigDecimal maker_volume; + private BigDecimal taker_volume; + private BigDecimal maker_fee; + private BigDecimal taker_fee; + private BigDecimal maker_tokens; + private BigDecimal taker_tokens; + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/OpenOrdersResponse.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/OpenOrdersResponse.java new file mode 100644 index 00000000000..b85ccaf1c54 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/OpenOrdersResponse.java @@ -0,0 +1,33 @@ +package com.knowm.xchange.vertex.dto; + +public class OpenOrdersResponse { + + /* + + { + "status": "success", + "data": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_id": 1, + "orders": [ + { + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "price_x18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "2000000000", + "nonce": "1", + "order_type": "default", + "unfilled_amount": "1000000000000000000", + "digest": "0x0000000000000000000000000000000000000000000000000000000000000000", + "placed_at": 1682437739 + } + ] + } + + */ + + +} + + diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/PriceAndQuantity.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/PriceAndQuantity.java new file mode 100644 index 00000000000..20780b6092b --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/PriceAndQuantity.java @@ -0,0 +1,24 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.math.BigInteger; +import lombok.Getter; +import lombok.ToString; + +@JsonFormat(shape = JsonFormat.Shape.ARRAY) +@JsonPropertyOrder({"price", "quantity"}) +@Getter +@ToString +public class PriceAndQuantity { + + BigInteger price; + + BigInteger quantity; + + public PriceAndQuantity(@JsonProperty("price") BigInteger price, @JsonProperty("quantity") BigInteger quantity) { + this.price = price; + this.quantity = quantity; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Rewards.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Rewards.java new file mode 100644 index 00000000000..e68bc1397c2 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Rewards.java @@ -0,0 +1,22 @@ +package com.knowm.xchange.vertex.dto; + + +import lombok.Getter; +import lombok.ToString; + +/* + Uptime is in minutes, and volumes & fees are in USD + */ +@ToString +@Getter +public class Rewards { + + private int epoch; + private long start_time; + + private long period; + private AddressRewards[] address_rewards; + private GlobalRewards[] global_rewards; + + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/RewardsList.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/RewardsList.java new file mode 100644 index 00000000000..8a41b1fb403 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/RewardsList.java @@ -0,0 +1,17 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.knowm.xchange.vertex.NanoSecondsDeserializer; +import java.time.Instant; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class RewardsList { + + private Rewards[] rewards; + + @JsonDeserialize(using = NanoSecondsDeserializer.class) + private Instant update_time; +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/RewardsRequest.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/RewardsRequest.java new file mode 100644 index 00000000000..e124be85a61 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/RewardsRequest.java @@ -0,0 +1,27 @@ +package com.knowm.xchange.vertex.dto; + +import lombok.Getter; +import lombok.ToString; + +@ToString +@Getter +public class RewardsRequest { + + + private final RewardAddress rewards; + + public RewardsRequest(RewardAddress rewards) { + this.rewards = rewards; + } + + @ToString + @Getter + public static class RewardAddress { + + private final String address; + + public RewardAddress(String address) { + this.address = address; + } + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Symbol.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Symbol.java new file mode 100644 index 00000000000..5268c1ea8ba --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Symbol.java @@ -0,0 +1,14 @@ +package com.knowm.xchange.vertex.dto; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class Symbol { + + int product_id; + + String symbol; + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Tx.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Tx.java new file mode 100644 index 00000000000..3b95a121717 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/Tx.java @@ -0,0 +1,33 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class Tx { + + private final String sender; + private final long[] productIds; + private final String[] digests; + private final String nonce; + + public Tx(@JsonProperty("sender") String sender, @JsonProperty("productIds") long[] productIds, @JsonProperty("digests") String[] digests, @JsonProperty("nonce") String nonce) { + this.sender = sender; + this.productIds = productIds; + this.digests = digests; + this.nonce = nonce; + } + + @JsonInclude() + public long[] getProductIds() { + return productIds; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public String[] getDigests() { + return digests; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexBestBidOfferMessage.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexBestBidOfferMessage.java new file mode 100644 index 00000000000..9b4c66e9238 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexBestBidOfferMessage.java @@ -0,0 +1,39 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.knowm.xchange.vertex.NanoSecondsDeserializer; +import java.math.BigInteger; +import java.time.Instant; +import lombok.Getter; +import lombok.ToString; + +@ToString +@Getter +public class VertexBestBidOfferMessage { + + + private final Instant timestamp; + private final long product_id; + private final BigInteger bid_price; + private final BigInteger bid_qty; + private final BigInteger ask_price; + private final BigInteger ask_qty; + + + public VertexBestBidOfferMessage(@JsonProperty("timestamp") @JsonDeserialize(using = NanoSecondsDeserializer.class) Instant timestamp, + @JsonProperty("product_id") long product_id, + @JsonProperty("bid_price") BigInteger bid_price, + @JsonProperty("bid_qty") BigInteger bid_qty, + @JsonProperty("ask_price") BigInteger ask_price, + @JsonProperty("ask_qty") BigInteger ask_qty) { + this.timestamp = timestamp; + this.product_id = product_id; + this.bid_price = bid_price; + this.bid_qty = bid_qty; + this.ask_price = ask_price; + this.ask_qty = ask_qty; + } + + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexCancelOrdersMessage.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexCancelOrdersMessage.java new file mode 100644 index 00000000000..f0376dd6a65 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexCancelOrdersMessage.java @@ -0,0 +1,33 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class VertexCancelOrdersMessage implements VertexRequest { + + private final CancelOrders cancelOrders; + + public VertexCancelOrdersMessage(@JsonProperty("cancel_orders") CancelOrders cancelOrders) { + this.cancelOrders = cancelOrders; + } + + @JsonProperty("cancel_orders") + public CancelOrders getCancelOrders() { + return cancelOrders; + } + + @Override + public String getRequestType() { + return "execute_cancel_orders"; + } + + @Override + public String getSignature() { + return cancelOrders.getSignature(); + } +} + + diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexCancelProductOrdersMessage.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexCancelProductOrdersMessage.java new file mode 100644 index 00000000000..889cffb43ee --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexCancelProductOrdersMessage.java @@ -0,0 +1,28 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.ToString; + +@ToString +public class VertexCancelProductOrdersMessage implements VertexRequest { + private final CancelProductOrders cancelProductOrders; + + public VertexCancelProductOrdersMessage(CancelProductOrders cancelProductOrders) { + this.cancelProductOrders = cancelProductOrders; + } + + @JsonProperty("cancel_product_orders") + public CancelProductOrders getCancelProductOrders() { + return cancelProductOrders; + } + + @Override + public String getRequestType() { + return "execute_cancel_product_orders"; + } + + @Override + public String getSignature() { + return cancelProductOrders.getSignature(); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexFillData.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexFillData.java new file mode 100644 index 00000000000..1402461df9a --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexFillData.java @@ -0,0 +1,66 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.knowm.xchange.vertex.NanoSecondsDeserializer; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.convertToDecimal; +import java.math.BigInteger; +import java.time.Instant; +import lombok.Getter; +import lombok.ToString; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.Trade; + +@Getter +@ToString +public class VertexFillData { + + + private final Instant timestamp; + private final String productId; + private final String orderId; + private final String subAccount; + + private final BigInteger filledQty; + private final BigInteger remainingQty; + private final BigInteger originalQty; + private final BigInteger price; + private final Boolean isBid; + private final Boolean isTaker; + + + public VertexFillData(@JsonProperty("timestamp") @JsonDeserialize(using = NanoSecondsDeserializer.class) Instant timestamp, + @JsonProperty("product_id") String productId, + @JsonProperty("order_digest") String orderId, + @JsonProperty("subaccount") String subAccount, + @JsonProperty("filled_qty") BigInteger filledQty, + @JsonProperty("remaining_qty") BigInteger remainingQty, + @JsonProperty("original_qty") BigInteger originalQty, + @JsonProperty("price") BigInteger price, + @JsonProperty("") Boolean isBid, + @JsonProperty("is_taker") Boolean isTaker) { + this.timestamp = timestamp; + this.productId = productId; + this.orderId = orderId; + this.subAccount = subAccount; + this.filledQty = filledQty; + this.remainingQty = remainingQty; + this.originalQty = originalQty; + this.price = price; + this.isBid = isBid; + this.isTaker = isTaker; + } + + public Trade toTrade(CurrencyPair currencyPair) { + Trade.Builder builder = new Trade.Builder() + .instrument(currencyPair) + .price(convertToDecimal(price)); + if (isTaker) { + builder.takerOrderId(orderId); + } else { + builder.makerOrderId(orderId); + } + builder.originalAmount(convertToDecimal(filledQty)); + return builder.build(); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexMarketDataUpdateMessage.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexMarketDataUpdateMessage.java new file mode 100644 index 00000000000..f2ff8a9b222 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexMarketDataUpdateMessage.java @@ -0,0 +1,44 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.knowm.xchange.vertex.NanoSecondsDeserializer; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class VertexMarketDataUpdateMessage { + + public static final List EMPTY_EVENTS = new ArrayList<>(); + public static final VertexMarketDataUpdateMessage EMPTY = new VertexMarketDataUpdateMessage(EMPTY_EVENTS, EMPTY_EVENTS, null, null, null, -1); + + private final List bids; + private final List asks; + private final Instant minTime; + private final Instant maxTime; + private final Instant lastMaxTime; + private final long productId; + + public VertexMarketDataUpdateMessage(@JsonProperty("bids") List bids, + @JsonProperty("asks") List asks, + @JsonProperty("min_timestamp") @JsonDeserialize(using = NanoSecondsDeserializer.class) Instant minTime, + @JsonProperty("max_timestamp") @JsonDeserialize(using = NanoSecondsDeserializer.class) Instant maxTime, + @JsonProperty("last_max_timestamp") @JsonDeserialize(using = NanoSecondsDeserializer.class) Instant lastMaxTime, + @JsonProperty("product_id") long productId) { + this.bids = bids; + this.asks = asks; + this.minTime = minTime; + this.maxTime = maxTime; + this.lastMaxTime = lastMaxTime; + this.productId = productId; + } + + public static VertexMarketDataUpdateMessage empty() { + return EMPTY; + } + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexModelUtils.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexModelUtils.java new file mode 100644 index 00000000000..be6f429bbf2 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexModelUtils.java @@ -0,0 +1,56 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.util.Iterator; +import java.util.List; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.web3j.utils.Numeric; + +public class VertexModelUtils { + public static final BigDecimal NUMBER_CONVERSION_FACTOR = BigDecimal.ONE.scaleByPowerOfTen(18); + + public static BigDecimal convertToDecimal(BigInteger integer) { + return new BigDecimal(integer).divide(NUMBER_CONVERSION_FACTOR); + } + + public static BigInteger convertToInteger(BigDecimal decimal) { + return decimal.multiply(NUMBER_CONVERSION_FACTOR).toBigInteger(); + } + + public static String buildNonce(int timeoutMillis) { + return String.valueOf((Instant.now().toEpochMilli() + timeoutMillis << 20) + RandomUtils.nextInt(1, 10000)); + } + + public static String buildSender(String walletAddress, String subAccount) { + byte[] walletBytes = Numeric.hexStringToByteArray(walletAddress); + if (walletBytes.length != 20) { + throw new IllegalArgumentException("Wallet address must be 20 bytes long, got " + walletBytes.length + ": " + walletAddress); + } + byte[] paddedSubAccount = StringUtils.isEmpty(subAccount) ? new byte[0] : subAccount.getBytes(); + + //append byte arrays + byte[] sender = new byte[32]; + System.arraycopy(walletBytes, 0, sender, 0, walletBytes.length); + System.arraycopy(paddedSubAccount, 0, sender, walletBytes.length, paddedSubAccount.length); + + return Numeric.toHexString(sender); + } + + public static BigDecimal readX18Decimal(JsonNode obj, String fieldName) { + return convertToDecimal(new BigInteger(obj.get(fieldName).asText())); + } + + public static void readX18DecimalArray(JsonNode node, String fieldName, List outputList) { + ArrayNode jsonNode = node.withArray(fieldName); + Iterator elements = jsonNode.elements(); + while (elements.hasNext()) { + JsonNode next = elements.next(); + outputList.add(convertToDecimal(new BigInteger(next.asText()))); + } + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexOrder.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexOrder.java new file mode 100644 index 00000000000..f799c0e51bd --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexOrder.java @@ -0,0 +1,24 @@ +package com.knowm.xchange.vertex.dto; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class VertexOrder { + + private final String sender; + private final String priceX18; + private final String amount; + private final String expiration; + private final String nonce; + + public VertexOrder(String sender, String priceX18, String amount, String expiration, String nonce) { + this.sender = sender; + this.priceX18 = priceX18; + this.amount = amount; + this.expiration = expiration; + this.nonce = nonce; + } + +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexOrderBookStream.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexOrderBookStream.java new file mode 100644 index 00000000000..1dcb5cd0e23 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexOrderBookStream.java @@ -0,0 +1,159 @@ +package com.knowm.xchange.vertex.dto; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.functions.Consumer; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.Subject; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentSkipListMap; +import lombok.Getter; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.marketdata.OrderBook; +import org.knowm.xchange.dto.trade.LimitOrder; +import org.knowm.xchange.instrument.Instrument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Getter +public class VertexOrderBookStream extends Observable implements Consumer { + private static final Logger logger = LoggerFactory.getLogger(VertexOrderBookStream.class); + + private final Subject orderBookSubject = PublishSubject.create().toSerialized(); + + private final Map bidPriceToBidQuantity = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); + private final Map offerPriceToOfferQuantity = new ConcurrentSkipListMap<>(); + private final Instrument instrument; + private final int maxDepth; + + public VertexOrderBookStream(Instrument instrument, int maxDepth) { + this.instrument = instrument; + this.maxDepth = maxDepth; + } + + @Override + protected void subscribeActual(Observer observer) { + orderBookSubject.subscribe(observer); + } + + @Override + public synchronized void accept(VertexMarketDataUpdateMessage updateMessage) { + if (updateMessage.getLastMaxTime() == null) { + handleSnapshot(updateMessage); + } else { + handleIncrement(updateMessage); + } + + publishOrderBookFromUpdate(updateMessage.getMaxTime()); + } + + private void processSnapshotOrders(List orders, Map mapForInsert, Order.OrderType type, Instant timestamp) { + mapForInsert.clear(); + + if (orders != null) { + for (PriceAndQuantity order : orders) { + + BigInteger price = order.getPrice(); + BigInteger quantity = order.getQuantity(); + + mapForInsert.put(price, getLimitOrder(type, instrument, timestamp, price, VertexModelUtils.convertToDecimal(quantity))); + } + } + } + + private void handleIncrement(VertexMarketDataUpdateMessage updateMessage) { + processIncrementOrders(updateMessage.getBids(), bidPriceToBidQuantity, Order.OrderType.BID, updateMessage.getMaxTime()); + processIncrementOrders(updateMessage.getAsks(), offerPriceToOfferQuantity, Order.OrderType.ASK, updateMessage.getMaxTime()); + } + + private void processIncrementOrders(List orders, Map mapForInsert, Order.OrderType type, Instant timestamp) { + if (orders != null) { + for (PriceAndQuantity order : orders) { + + BigInteger price = order.getPrice(); + BigInteger quantityAsInt = order.getQuantity(); + + if (isZero(quantityAsInt)) { + mapForInsert.remove(price); + } else { + LimitOrder exising = mapForInsert.get(price); + BigDecimal quantity = VertexModelUtils.convertToDecimal(quantityAsInt); + if (exising != null && exising.getOriginalAmount().equals(quantity)) { + continue; + } + LimitOrder limitOrder = getLimitOrder(type, instrument, timestamp, price, quantity); + mapForInsert.put(price, limitOrder); + } + + } + } + } + + private static boolean isZero(BigInteger quantityAsInt) { + return quantityAsInt.compareTo(BigInteger.ZERO) == 0; + } + + private void publishOrderBookFromUpdate(Instant timestamp) { + OrderBook book = generateOrderBook(timestamp); + + orderBookSubject.onNext(book); + } + + private void populateOrders(List orders, Map priceToOrder) { + int currentDepth = 0; + for (Map.Entry bigDecimalBigDecimalEntry : priceToOrder.entrySet()) { + LimitOrder order = bigDecimalBigDecimalEntry.getValue(); + orders.add(order); + + currentDepth += 1; + + if (currentDepth == maxDepth) break; + } + } + + private static LimitOrder getLimitOrder(Order.OrderType type, Instrument instrument, Instant timestamp, BigInteger priceAsInt, BigDecimal quantity) { + BigDecimal price = VertexModelUtils.convertToDecimal(priceAsInt); + + return new LimitOrder(type, quantity, instrument, null, new Date(timestamp.toEpochMilli()), price); + } + + private void handleSnapshot(VertexMarketDataUpdateMessage updateMessage) { + logger.info("{} - depth {}: Received snapshot, clearing order book and repopulating.", instrument, maxDepth); + processSnapshotOrders(updateMessage.getBids(), bidPriceToBidQuantity, Order.OrderType.BID, updateMessage.getMaxTime()); + processSnapshotOrders(updateMessage.getAsks(), offerPriceToOfferQuantity, Order.OrderType.ASK, updateMessage.getMaxTime()); + } + + + private OrderBook generateOrderBook(Instant instant) { + int capacity = maxDepth != Integer.MAX_VALUE ? maxDepth : 50; + List bids = new ArrayList<>(capacity); + List offers = new ArrayList<>(capacity); + + Date timestamp = new Date(instant == null ? Instant.now().toEpochMilli() : instant.toEpochMilli()); + populateOrders(bids, bidPriceToBidQuantity); + populateOrders(offers, offerPriceToOfferQuantity); + + return new OrderBook( + new Date(), + timestamp, + offers, + bids, + false + ); + } + + @Override + public String toString() { + return "VertexOrderBookStream{" + + "instrument=" + instrument + + ", maxDepth=" + maxDepth + + '}'; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexPlaceOrder.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexPlaceOrder.java new file mode 100644 index 00000000000..625c65a6240 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexPlaceOrder.java @@ -0,0 +1,26 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class VertexPlaceOrder { + private final long product_id; + private final VertexOrder order; + private final String signature; + private final Boolean spot_leverage; + + public VertexPlaceOrder(long productId, VertexOrder order, String signature, Boolean spotLeverage) { + product_id = productId; + this.order = order; + this.signature = signature; + spot_leverage = spotLeverage; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public Boolean getSpot_leverage() { + return spot_leverage; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexPlaceOrderMessage.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexPlaceOrderMessage.java new file mode 100644 index 00000000000..05baecf1bd3 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexPlaceOrderMessage.java @@ -0,0 +1,24 @@ +package com.knowm.xchange.vertex.dto; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class VertexPlaceOrderMessage implements VertexRequest { + private final VertexPlaceOrder place_order; + + public VertexPlaceOrderMessage(VertexPlaceOrder placeOrder) { + place_order = placeOrder; + } + + @Override + public String getRequestType() { + return "execute_place_order"; + } + + @Override + public String getSignature() { + return place_order.getSignature(); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexRequest.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexRequest.java new file mode 100644 index 00000000000..18d5644d5e6 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexRequest.java @@ -0,0 +1,12 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public interface VertexRequest { + + @JsonIgnore + String getRequestType(); + + @JsonIgnore + String getSignature(); +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexTradeData.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexTradeData.java new file mode 100644 index 00000000000..072f12d5c65 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/dto/VertexTradeData.java @@ -0,0 +1,53 @@ +package com.knowm.xchange.vertex.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.knowm.xchange.vertex.NanoSecondsDeserializer; +import static com.knowm.xchange.vertex.dto.VertexModelUtils.convertToDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.util.Date; +import lombok.Getter; +import lombok.ToString; +import org.knowm.xchange.dto.Order; +import org.knowm.xchange.dto.marketdata.Trade; +import org.knowm.xchange.instrument.Instrument; + +@Getter +@ToString +public class VertexTradeData { + + private final Instant timestamp; + private final String productId; + private final BigInteger makerQty; + private final BigInteger takerQty; + private final BigInteger price; + private final Boolean isTakerBuyer; + + public VertexTradeData(@JsonProperty("timestamp") @JsonDeserialize(using = NanoSecondsDeserializer.class) Instant timestamp, + @JsonProperty("product_id") String productId, + @JsonProperty("maker_qty") BigInteger makerQty, + @JsonProperty("taker_qty") BigInteger takerQty, + @JsonProperty("price") BigInteger price, + @JsonProperty("is_taker_buyer") Boolean isTakerBuyer) { + this.timestamp = timestamp; + this.productId = productId; + this.makerQty = makerQty; + this.takerQty = takerQty; + this.price = price; + this.isTakerBuyer = isTakerBuyer; + } + + + public Trade toTrade(Instrument currencyPair) { + Trade.Builder builder = new Trade.Builder() + .instrument(currencyPair) + .timestamp(new Date(timestamp.toEpochMilli())) + .price(convertToDecimal(price)) + .originalAmount(convertToDecimal(takerQty)) + .type(isTakerBuyer ? Order.OrderType.ASK : Order.OrderType.BID); // trade side from maker point of view + + builder.originalAmount(convertToDecimal(takerQty)); + return builder.build(); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Domain.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Domain.java new file mode 100644 index 00000000000..0dc5b44dc09 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Domain.java @@ -0,0 +1,35 @@ +package com.knowm.xchange.vertex.signing; + +public class EIP712Domain { + + private final String name; + + private final String version; + + private final long chainId; + + private final String verifyingContract; + + public EIP712Domain(String name, String version, long chainId, String verifyingContract) { + this.name = name; + this.version = version; + this.chainId = chainId; + this.verifyingContract = verifyingContract; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public long getChainId() { + return chainId; + } + + public String getVerifyingContract() { + return verifyingContract; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Schema.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Schema.java new file mode 100644 index 00000000000..6c0d29a5fe3 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Schema.java @@ -0,0 +1,52 @@ +package com.knowm.xchange.vertex.signing; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class EIP712Schema { + + public static final List DOMAIN_TYPE = List.of( + new EIP712Type("name", "string"), + new EIP712Type("version", "string"), + new EIP712Type("chainId", "uint256"), + new EIP712Type("verifyingContract", "address") + ); + + private final Map> types; + + private final String primaryType; + + private final EIP712Domain domain; + + private final HashMap message; + + public EIP712Schema(Map> types, String primaryType, EIP712Domain domain, Map message) { + this.types = new TreeMap<>(types); + this.types.put("EIP712Domain", DOMAIN_TYPE); + this.primaryType = primaryType; + this.message = new HashMap<>(message); + this.domain = domain; + } + + protected static EIP712Domain getDomain(long chainId, String verifyingContract) { + return new EIP712Domain("Vertex", "0.0.1", chainId, verifyingContract); + } + + public Map> getTypes() { + return types; + } + + public String getPrimaryType() { + return primaryType; + } + + public EIP712Domain getDomain() { + return domain; + } + + public HashMap getMessage() { + return message; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Type.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Type.java new file mode 100644 index 00000000000..d541592eec7 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/EIP712Type.java @@ -0,0 +1,21 @@ +package com.knowm.xchange.vertex.signing; + +public class EIP712Type { + + private final String name; + + private final String type; + + public EIP712Type(String name, String type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/MessageSigner.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/MessageSigner.java new file mode 100644 index 00000000000..68cb43a003c --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/MessageSigner.java @@ -0,0 +1,61 @@ +package com.knowm.xchange.vertex.signing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import info.bitrich.xchangestream.service.netty.StreamingObjectMapperHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.FixedStructuredDataEncoder; +import org.web3j.crypto.Sign; +import org.web3j.utils.Numeric; + +public class MessageSigner { + private static final Logger log = LoggerFactory.getLogger(MessageSigner.class); + private final ECKeyPair keyPair; + private final ObjectMapper mapper; + + public MessageSigner(String privateKey) { + + // load a key pair from a private key + keyPair = Credentials.create(privateKey).getEcKeyPair(); + + mapper = StreamingObjectMapperHelper.getObjectMapper(); + } + + + public SignatureAndDigest signMessage(EIP712Schema schema) { + + try { + + String jsonSchema = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema); + + log.trace("Signing message: {}", jsonSchema); + + FixedStructuredDataEncoder encoder = new FixedStructuredDataEncoder(jsonSchema); + // TODO submit bugs to web3J +// StructuredDataEncoder encoder = new StructuredDataEncoder(jsonSchema); + + byte[] bytes = encoder.hashStructuredData(); + + String digest = Numeric.toHexString(bytes); + log.trace("digest: {}", digest); + + // Sign the hashed message + Sign.SignatureData signatureData = Sign.signMessage(bytes, keyPair, false); + + // join the r, s and v fields into one byte array and encode to hex + byte[] signature = new byte[65]; + System.arraycopy(signatureData.getR(), 0, signature, 0, 32); + System.arraycopy(signatureData.getS(), 0, signature, 32, 32); + signature[64] = signatureData.getV()[0]; + + String sigString = Numeric.toHexString(signature); + return new SignatureAndDigest(sigString, digest); + + } catch (Throwable e) { + throw new RuntimeException(e); + } + + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/SignatureAndDigest.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/SignatureAndDigest.java new file mode 100644 index 00000000000..498f4f942a0 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/SignatureAndDigest.java @@ -0,0 +1,24 @@ +package com.knowm.xchange.vertex.signing; + +public class SignatureAndDigest { + private final String signature; + private final String digest; + + public SignatureAndDigest(String signature, String digest) { + this.signature = signature; + this.digest = digest; + } + + public String getSignature() { + return signature; + } + + public String getDigest() { + return digest; + } + + @Override + public String toString() { + return "Sig: " + signature + ", Digest: " + digest; + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/CancelOrdersSchema.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/CancelOrdersSchema.java new file mode 100644 index 00000000000..198693c0ebd --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/CancelOrdersSchema.java @@ -0,0 +1,49 @@ +package com.knowm.xchange.vertex.signing.schemas; + +import com.knowm.xchange.vertex.signing.EIP712Domain; +import com.knowm.xchange.vertex.signing.EIP712Schema; +import com.knowm.xchange.vertex.signing.EIP712Type; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import static java.util.Map.of; + +public class CancelOrdersSchema extends EIP712Schema { + + /* + + { + Cancellation: [ + { name: 'sender', type: 'bytes32' }, + { name: 'productIds', type: 'uint32[]' }, + { name: 'digests', type: 'bytes32[]' }, + { name: 'nonce', type: 'uint64' }, + ], +} + */ + + private CancelOrdersSchema(EIP712Domain domain, Map message) { + super(of("Cancellation", List.of( + new EIP712Type("sender", "bytes32"), + new EIP712Type("productIds", "uint32[]"), + new EIP712Type("digests", "bytes32[]"), + new EIP712Type("nonce", "uint64") + )), + "Cancellation", + domain, + message); + } + + public static CancelOrdersSchema build(long chainId, String verifyingContract, Long nonce, String sender, long[] productIds, String[] digests) { + + EIP712Domain domain = getDomain(chainId, verifyingContract); + + Map fields = new LinkedHashMap<>(); + fields.put("sender", sender); + fields.put("productIds", productIds); + fields.put("digests", digests); + fields.put("nonce", nonce); + + return new CancelOrdersSchema(domain, fields); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/CancelProductOrdersSchema.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/CancelProductOrdersSchema.java new file mode 100644 index 00000000000..1561adbc2c7 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/CancelProductOrdersSchema.java @@ -0,0 +1,40 @@ +package com.knowm.xchange.vertex.signing.schemas; + +import com.knowm.xchange.vertex.signing.EIP712Domain; +import com.knowm.xchange.vertex.signing.EIP712Schema; +import com.knowm.xchange.vertex.signing.EIP712Type; +import java.util.List; +import java.util.Map; + +public class CancelProductOrdersSchema extends EIP712Schema { + + /* + + 'CancellationProducts': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'productIds', 'type': 'uint32[]'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + */ + + private CancelProductOrdersSchema(EIP712Domain domain, Map message) { + super(Map.of( + "CancellationProducts", List.of( + new EIP712Type("sender", "bytes32"), + new EIP712Type("productIds", "uint32[]"), + new EIP712Type("nonce", "uint64") + )), + "CancellationProducts", + domain, + message); + } + + public static CancelProductOrdersSchema build(long chainId, String endpointContract, Long aLong, String sender, long[] productIds) { + EIP712Domain domain = getDomain(chainId, endpointContract); + Map message = Map.of( + "sender", sender, + "productIds", productIds, + "nonce", aLong); + return new CancelProductOrdersSchema(domain, message); + } +} diff --git a/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/PlaceOrderSchema.java b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/PlaceOrderSchema.java new file mode 100644 index 00000000000..b77bb2679a7 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/com/knowm/xchange/vertex/signing/schemas/PlaceOrderSchema.java @@ -0,0 +1,41 @@ +package com.knowm.xchange.vertex.signing.schemas; + +import com.knowm.xchange.vertex.signing.EIP712Domain; +import com.knowm.xchange.vertex.signing.EIP712Schema; +import com.knowm.xchange.vertex.signing.EIP712Type; +import java.math.BigInteger; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import static java.util.Map.of; + +public class PlaceOrderSchema extends EIP712Schema { + + + private PlaceOrderSchema(EIP712Domain domain, Map message) { + super(of("Order", List.of( + new EIP712Type("sender", "bytes32"), + new EIP712Type("priceX18", "int128"), + new EIP712Type("amount", "int128"), + new EIP712Type("expiration", "uint64"), + new EIP712Type("nonce", "uint64") + )), + "Order", + domain, + message); + } + + public static PlaceOrderSchema build(long chainId, String verifyingContract, Long nonce, String sender, BigInteger expiration, BigInteger quantityAsInt, BigInteger priceAsInt) { + EIP712Domain domain = getDomain(chainId, verifyingContract); + + Map fields = new LinkedHashMap<>(); + fields.put("sender", sender); + fields.put("priceX18", priceAsInt); + fields.put("amount", quantityAsInt); + fields.put("expiration", expiration); + fields.put("nonce", nonce); + + return new PlaceOrderSchema(domain, fields); + } + +} diff --git a/xchange-stream-vertex/src/main/java/org/web3j/crypto/FixedStructuredDataEncoder.java b/xchange-stream-vertex/src/main/java/org/web3j/crypto/FixedStructuredDataEncoder.java new file mode 100644 index 00000000000..ef196e22912 --- /dev/null +++ b/xchange-stream-vertex/src/main/java/org/web3j/crypto/FixedStructuredDataEncoder.java @@ -0,0 +1,154 @@ +package org.web3j.crypto; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import org.web3j.abi.TypeEncoder; +import org.web3j.abi.datatypes.AbiTypes; +import org.web3j.abi.datatypes.Type; +import static org.web3j.crypto.Hash.sha3; +import static org.web3j.crypto.Hash.sha3String; +import org.web3j.utils.Numeric; + +public class FixedStructuredDataEncoder extends org.web3j.crypto.StructuredDataEncoder { + public FixedStructuredDataEncoder(String jsonMessageInString) throws IOException, RuntimeException { + super(jsonMessageInString); + } + + @Override + public List getArrayDimensionsFromDeclaration(String declaration) { + // All arrays are dynamic, bug in web3j does not detect these properly + return List.of(-1); + } + + // Bug in web3j does not encode primitive arrays properly (tries to cast value to Map of fields) + public byte[] encodeData(String primaryType, HashMap data) + throws RuntimeException { + HashMap> types = jsonMessageObject.getTypes(); + + List encTypes = new ArrayList<>(); + List encValues = new ArrayList<>(); + + // Add typehash + encTypes.add("bytes32"); + encValues.add(typeHash(primaryType)); + + // Add field contents + for (StructuredData.Entry field : types.get(primaryType)) { + Object value = data.get(field.getName()); + + if (field.getType().equals("string")) { + encTypes.add("bytes32"); + byte[] hashedValue = Numeric.hexStringToByteArray(sha3String((String) value)); + encValues.add(hashedValue); + } else if (field.getType().equals("bytes")) { + encTypes.add(("bytes32")); + encValues.add(sha3(Numeric.hexStringToByteArray((String) value))); + } else if (types.containsKey(field.getType())) { + // User Defined Type + byte[] hashedValue = + sha3(encodeData(field.getType(), (HashMap) value)); + encTypes.add("bytes32"); + encValues.add(hashedValue); + } else if (bytesTypePattern.matcher(field.getType()).find()) { + encTypes.add(field.getType()); + encValues.add(Numeric.hexStringToByteArray((String) value)); + } else if (arrayTypePattern.matcher(field.getType()).find()) { + String baseTypeName = field.getType().substring(0, field.getType().indexOf('[')); + List expectedDimensions = + getArrayDimensionsFromDeclaration(field.getType()); + // This function will itself give out errors in case + // that the data is not a proper array + List dataDimensions = getArrayDimensionsFromData(value); + + final String format = + String.format( + "Array Data %s has dimensions %s, " + + "but expected dimensions are %s", + value.toString(), + dataDimensions.toString(), + expectedDimensions.toString()); + if (expectedDimensions.size() != dataDimensions.size()) { + // Ex: Expected a 3d array, but got only a 2d array + throw new RuntimeException(format); + } + for (int i = 0; i < expectedDimensions.size(); i++) { + if (expectedDimensions.get(i) == -1) { + // Skip empty or dynamically declared dimensions + continue; + } + if (!expectedDimensions.get(i).equals(dataDimensions.get(i))) { + throw new RuntimeException(format); + } + } + + List arrayItems = flattenMultidimensionalArray(value); + + List arrayTypes = arrayItems.stream().map((x) -> baseTypeName).collect(Collectors.toList()); + arrayItems = arrayItems.stream().map((x) -> { + if (x instanceof String) { + return Numeric.hexStringToByteArray((String) x); + } else { + return x; + } + }).collect(Collectors.toList()); + + byte[] encodedItems = encodeTypesAndValues(arrayTypes, arrayItems); + byte[] hashedValue = sha3(encodedItems); + encTypes.add("bytes32"); + encValues.add(hashedValue); + } else { + encTypes.add(field.getType()); + encValues.add(value); + } + } + + return encodeTypesAndValues(encTypes, encValues); + } + + private static byte[] encodeTypesAndValues(List encTypes, List encValues) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (int i = 0; i < encTypes.size(); i++) { + Class typeClazz = (Class) AbiTypes.getType(encTypes.get(i)); + + boolean atleastOneConstructorExistsForGivenParametersType = false; + // Using the Reflection API to get the types of the parameters + Constructor[] constructors = typeClazz.getConstructors(); + for (Constructor constructor : constructors) { + // Check which constructor matches + try { + Class[] parameterTypes = constructor.getParameterTypes(); + byte[] temp = + Numeric.hexStringToByteArray( + TypeEncoder.encode( + typeClazz + .getDeclaredConstructor(parameterTypes) + .newInstance(encValues.get(i)))); + baos.write(temp, 0, temp.length); + atleastOneConstructorExistsForGivenParametersType = true; + break; + } catch (IllegalArgumentException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException + | InvocationTargetException ignored) { + } + } + + if (!atleastOneConstructorExistsForGivenParametersType) { + throw new RuntimeException( + String.format( + "Received an invalid argument for which no constructor" + + " exists for the ABI Class %s", + typeClazz.getSimpleName())); + } + } + byte[] result = baos.toByteArray(); + return result; + } +} diff --git a/xchange-stream-vertex/src/main/resources/logback.xml b/xchange-stream-vertex/src/main/resources/logback.xml new file mode 100644 index 00000000000..ce56cfb7cd9 --- /dev/null +++ b/xchange-stream-vertex/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + + + + %d{HH:mm:ss.SSS} [%contextName] [%thread] %-5level %logger{36} - %msg %xEx%n + + + + + + + + + + + + + + diff --git a/xchange-stream-vertex/src/main/resources/vertex.json b/xchange-stream-vertex/src/main/resources/vertex.json new file mode 100644 index 00000000000..8544f1ae164 --- /dev/null +++ b/xchange-stream-vertex/src/main/resources/vertex.json @@ -0,0 +1,45 @@ +{ + "currency_pairs": { + "wBTC/USDC": { + "price_scale": 4, + "min_amount": 0.001 + }, + "wETH/USDC": { + "price_scale": 4, + "min_amount": 0.01 + }, + "ETH/PERP": { + "price_scale": 4, + "min_amount": 0.01 + }, + "BTC/PERP": { + "price_scale": 4, + "min_amount": 0.001 + } + }, + "currencies": { + "wBTC": { + "scale": "4", + "withdrawal_fee": 0 + }, + "wETH": { + "scale": "", + "withdrawal_fee": "" + } + }, + "public_rate_limits": [ + { + "calls": 60, + "time_span": 10, + "time_unit": "seconds" + } + ], + "private_rate_limits": [ + { + "calls": 60, + "time_span": 10, + "time_unit": "seconds" + } + ], + "share_rate_limits": true +} diff --git a/xchange-stream-vertex/src/test/java/com/knowm/xchange/vertex/MessageSignerTest.java b/xchange-stream-vertex/src/test/java/com/knowm/xchange/vertex/MessageSignerTest.java new file mode 100644 index 00000000000..cc6c66dfe1a --- /dev/null +++ b/xchange-stream-vertex/src/test/java/com/knowm/xchange/vertex/MessageSignerTest.java @@ -0,0 +1,74 @@ +package com.knowm.xchange.vertex; + +import com.knowm.xchange.vertex.dto.VertexModelUtils; +import com.knowm.xchange.vertex.signing.MessageSigner; +import com.knowm.xchange.vertex.signing.schemas.CancelOrdersSchema; +import com.knowm.xchange.vertex.signing.schemas.PlaceOrderSchema; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; +import org.web3j.utils.Numeric; + +public class MessageSignerTest { + + Logger log = LoggerFactory.getLogger(MessageSignerTest.class); + + @Test + public void testSignOrder() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException { + + ECKeyPair ecKeyPair = Keys.createEcKeyPair(); + + MessageSigner messageSigner = new MessageSigner(ecKeyPair.getPrivateKey().toString(16)); + + + PlaceOrderSchema orderSchema = PlaceOrderSchema.build(421613, "0xf03f457a30e598d5020164a339727ef40f2b8fbc", 1L, VertexModelUtils.buildSender("0x3cd04f7Dbef1DE0C27100536CE12819Ee9dCFAC3", ""), BigInteger.ZERO, + BigInteger.valueOf(10000000000L), BigInteger.valueOf(10000000000L)); + String signatureData = messageSigner.signMessage(orderSchema).getSignature(); + + log.info("signatureData: {}", signatureData); + + + } + + + @Test + public void examplePlaceOrder() { + + ECKeyPair ecKeyPair = Credentials.create("09093d55d404c51871cc12a73fc482a245bb066d101d1ac840d73ee534cee4b9").getEcKeyPair(); + + MessageSigner messageSigner = new MessageSigner(ecKeyPair.getPrivateKey().toString(16)); + + BigInteger zero = BigInteger.valueOf(4611687701117784255L); + + String sender = Numeric.toHexString(Numeric.hexStringToByteArray("0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000")); + PlaceOrderSchema orderSchema = PlaceOrderSchema.build(421613, "0xf03f457a30e598d5020164a339727ef40f2b8fbc", 1764428860167815857L, + sender, zero, BigInteger.valueOf(-10000000000000000L), new BigInteger("28898000000000000000000")); + String signatureData = messageSigner.signMessage(orderSchema).getSignature(); + + assertEquals("0x4ed2c9e3e8d5dd331d980d0cb7effc8f007b5cc81159c3c0c5cdffb2249de1710e6f7d398fd57b5cab32146b88c8bae1ae74ca5f23dd066779d35166aafa4fb21b", signatureData); + + } + + @Test + public void exampleCancelOrder() { + ECKeyPair ecKeyPair = Credentials.create("09093d55d404c51871cc12a73fc482a245bb066d101d1ac840d73ee534cee4b9").getEcKeyPair(); + + MessageSigner messageSigner = new MessageSigner(ecKeyPair.getPrivateKey().toString(16)); + + String sender = Numeric.toHexString(Numeric.hexStringToByteArray("0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000")); + CancelOrdersSchema orderSchema = CancelOrdersSchema.build(421613, + "0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b", 1L, sender, new long[]{4}, new String[]{"0x51ba8762bc5f77957a4e896dba34e17b553b872c618ffb83dba54878796f2821"}); + String signatureData = messageSigner.signMessage(orderSchema).getSignature(); + + assertEquals("0x940651c03ee3201de3b3f46d772f31d0ac276eff1b2c8b7a6b0c159e9005c2fb6b78b6722692c05445aa76d231331f3f8f2f654b278a3af9794979121324f5741b", signatureData); + + } +} diff --git a/xchange-stream-vertex/src/test/java/com/knowm/xchange/vertex/VertexTickerIntegration.java b/xchange-stream-vertex/src/test/java/com/knowm/xchange/vertex/VertexTickerIntegration.java new file mode 100644 index 00000000000..c1c1adf0b83 --- /dev/null +++ b/xchange-stream-vertex/src/test/java/com/knowm/xchange/vertex/VertexTickerIntegration.java @@ -0,0 +1,48 @@ +package com.knowm.xchange.vertex; + +import info.bitrich.xchangestream.core.StreamingMarketDataService; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.knowm.xchange.Exchange; +import org.knowm.xchange.ExchangeSpecification; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.Ticker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VertexTickerIntegration { + + private static final Logger logger = LoggerFactory.getLogger(VertexTickerIntegration.class); + + + @Test + public void subscribesToTicker() throws InterruptedException { + VertexStreamingExchange exchange = new VertexStreamingExchange(); + ExchangeSpecification spec = exchange.getDefaultExchangeSpecification(); + spec.setExchangeSpecificParametersItem(Exchange.USE_SANDBOX, false); + exchange.applySpecification(spec); + StreamingMarketDataService streamingMarketDataService = exchange.getStreamingMarketDataService(); + + Observable ticker = streamingMarketDataService.getTicker(new CurrencyPair("BTC-PERP", "USDC")); + + assertTrue(exchange.connect().blockingAwait(10, TimeUnit.SECONDS)); + + CountDownLatch ticks = new CountDownLatch(1); + Disposable subscription = ticker.subscribe((tick) -> { + logger.info("Tick: " + tick); + ticks.countDown(); + }, (error) -> logger.error("Ticker error", error)); + try { + + assertTrue(ticks.await(30, TimeUnit.SECONDS)); + } finally { + subscription.dispose(); + assertTrue(exchange.disconnect().blockingAwait(10, TimeUnit.SECONDS)); + } + } + +} diff --git a/xchange-stream-vertex/src/test/python/sign_test.py b/xchange-stream-vertex/src/test/python/sign_test.py new file mode 100644 index 00000000000..e63b90093d8 --- /dev/null +++ b/xchange-stream-vertex/src/test/python/sign_test.py @@ -0,0 +1,99 @@ +from eth_account import Account +from eth_account.messages import encode_structured_data, _hash_eip191_message +from pprint import pprint + +# Replace this with your Ethereum private key +private_key = '09093d55d404c51871cc12a73fc482a245bb066d101d1ac840d73ee534cee4b9' + + +def hex_to_bytes32(hex_string): + if hex_string.startswith("0x"): + hex_string = hex_string[2:] + data_bytes = bytes.fromhex(hex_string) + padded_data = b'\x00' * (32 - len(data_bytes)) + data_bytes + return padded_data + + +sender = hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000') + +# EIP-712 Typed Data +typed_data = { + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'Order': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'priceX18', 'type': 'int128'}, + {'name': 'amount', 'type': 'int128'}, + {'name': 'expiration', 'type': 'uint64'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'Order', + 'domain': { + 'name': 'Vertex', + 'version': '0.0.1', + 'chainId': 421613, + 'verifyingContract': '0xf03f457a30e598d5020164a339727ef40f2b8fbc' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'priceX18': 28898000000000000000000, + 'amount': -10000000000000000, + 'expiration': 4611687701117784255, + 'nonce': 1764428860167815857, + }, +} + +typed_data2 = { + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'Cancellation': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'productIds', 'type': 'uint32[]'}, + {'name': 'digests', 'type': 'bytes32[]'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'Cancellation', + 'domain': { + 'name': 'Vertex', + 'version': '0.0.1', + 'chainId': 421613, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'productIds': [4], + 'digests': [hex_to_bytes32('0x51ba8762bc5f77957a4e896dba34e17b553b872c618ffb83dba54878796f2821')], + 'nonce': 1, + }, +} + + +def sign_typed_data(typed_data, private_key): + account: Account = Account.from_key(private_key) + encoded_data = encode_structured_data(typed_data) + digest = _hash_eip191_message(encoded_data) + typed_data_hash = account.sign_message(encoded_data) + return typed_data_hash.signature.hex(), digest.hex() + + +pprint(typed_data) +signature, digest = sign_typed_data(typed_data, private_key) +print("Signature", signature) +print("digest", digest) + +pprint(typed_data2) +signature, digest = sign_typed_data(typed_data2, private_key) +print("Signature", signature) +print("digest", digest) diff --git a/xchange-stream-vertex/src/test/resources/logback.xml b/xchange-stream-vertex/src/test/resources/logback.xml new file mode 100644 index 00000000000..3de418c482a --- /dev/null +++ b/xchange-stream-vertex/src/test/resources/logback.xml @@ -0,0 +1,13 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + +