diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index aa241b3bd..4768dab35 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,4 +1,4 @@ -name: Deploy OPEX - Dev +name: Push images on dev branch update on: push: @@ -6,14 +6,35 @@ on: - dev jobs: - jenkinsJob: - name: Build OPEX new dev version - runs-on: ubuntu-latest + build: + runs-on: ubuntu-20.04 + strategy: + matrix: + java: [ 11 ] + name: Build OPEX and run tests with java ${{ matrix.java }} steps: - - name: Trigger opex-build-job on jenkins - uses: appleboy/jenkins-action@master + - name: Checkout Source Code + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v2 with: - url: ${{ secrets.JENKINS_URL }} - user: ${{ secrets.JENKINS_USER }} - token: ${{ secrets.JENKINS_TOKEN }} - job: "opex-core-dev-deploy" + distribution: 'adopt' + java-package: jdk + java-version: ${{ matrix.java }} + cache: maven + - name: Build + run: mvn -B -T 1C clean install + - name: Build Docker images + env: + TAG: dev + run: docker-compose -f docker-compose.build.yml build + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Push images to GitHub Container Registry + env: + TAG: dev + run: docker-compose -f docker-compose.build.yml push diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 74a80f71d..a4eed9c0a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Deploy OPEX - Demo +name: Push images on main branch update on: push: @@ -6,14 +6,35 @@ on: - main jobs: - jenkinsJob: - name: Deploy OPEX new demo version - runs-on: ubuntu-latest + build: + runs-on: ubuntu-20.04 + strategy: + matrix: + java: [ 11 ] + name: Build OPEX and run tests with java ${{ matrix.java }} steps: - - name: Trigger opex-build-job on jenkins - uses: appleboy/jenkins-action@master + - name: Checkout Source Code + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v2 with: - url: ${{ secrets.JENKINS_URL }} - user: ${{ secrets.JENKINS_USER }} - token: ${{ secrets.JENKINS_TOKEN }} - job: "opex-core-demo-deploy" + distribution: 'adopt' + java-package: jdk + java-version: ${{ matrix.java }} + cache: maven + - name: Build + run: mvn -B -T 1C clean install + - name: Build Docker images + env: + TAG: latest + run: docker-compose -f docker-compose.build.yml build + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Push images to GitHub Container Registry + env: + TAG: latest + run: docker-compose -f docker-compose.build.yml push diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5d69ee53d..c2ca70829 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,4 +1,4 @@ -name: Build on pull request +name: Build and test on pull request on: pull_request: @@ -10,7 +10,7 @@ jobs: strategy: matrix: java: [ 11 ] - name: Opex with java ${{ matrix.java }} build + name: Build OPEX and run tests with java ${{ matrix.java }} steps: - name: Checkout Source Code uses: actions/checkout@v2 @@ -21,6 +21,9 @@ jobs: java-package: jdk java-version: ${{ matrix.java }} cache: maven - - name: Build OPEX-Core - working-directory: . - run: mvn -B clean install + - name: Build + run: mvn -B -T 1C clean install + - name: Build Docker images + env: + TAG: latest + run: docker-compose -f docker-compose.build.yml build diff --git a/.gitignore b/.gitignore index 8ccbd2f56..19d7407a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,43 +1,45 @@ -HELP.md -target/ -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### Other files ### -.env -docker-compose.local.yml -application-local.yml -mvnw -mvnw.cmd - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ -!/.mvn/ - -.DS_Store +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### Other files ### +.env +docker-compose.local.yml +docker-compose.legacy.yml +application-local.yml +mvnw +mvnw.cmd + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +!/.mvn/ + +.DS_Store +resources diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index b8af7f8c9..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,72 +0,0 @@ -pipeline { - agent any - - stages('Deploy') { - stage('Build') { - steps { - setBuildStatus("?", "PENDING") - withMaven( - maven: 'maven-3.6.3' - ) { - sh 'mvn -T 1C -B clean install' - } - } - } - stage('Deliver') { - environment { - DATA = '/var/opex/demo-core' - PANEL_PASS = credentials("v-panel-secret") - BACKEND_USER = credentials("v-backend-secret") - SMTP_PASS = credentials("smtp-secret") - DB_USER = 'opex' - DB_PASS = credentials("db-secret") - DB_BACKUP_USER = 'opex_backup' - DB_BACKUP_PASS = credentials("db-backup-secret") - KEYCLOAK_ADMIN_URL = 'https://demo.opex.dev/auth' - KEYCLOAK_FRONTEND_URL = 'https://demo.opex.dev/auth' - KEYCLOAK_ADMIN_USERNAME = credentials("keycloak-admin-username") - KEYCLOAK_ADMIN_PASSWORD = credentials("keycloak-admin-password") - OPEX_ADMIN_KEYCLOAK_CLIENT_SECRET = credentials("opex-admin-keycloak-client-secret") - VANDAR_API_KEY = credentials("vandar-api-key") - COMPOSE_PROJECT_NAME = 'demo-core' - DEFAULT_NETWORK_NAME = 'demo-opex' - } - steps { - sh 'docker-compose up -d --build --remove-orphans' - sh 'docker image prune -f' - sh 'docker network prune -f' - } - } - } - - post { - always { - echo 'One way or another, I have finished' - } - success { - echo ':)' - setBuildStatus(":)", "SUCCESS") - } - unstable { - echo ':/' - setBuildStatus(":/", "UNSTABLE") - } - failure { - echo ':(' - setBuildStatus(":(", "FAILURE") - } - changed { - echo 'Things were different before...' - } - } -} - -void setBuildStatus(String message, String state) { - step([ - $class : "GitHubCommitStatusSetter", - reposSource : [$class: "ManuallyEnteredRepositorySource", url: "https://github.com/opexdev/OPEX-Core"], - contextSource : [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/build-status"], - errorHandlers : [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]], - statusResultSource: [$class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]]] - ]) -} diff --git a/README.md b/README.md index f40dc44c6..e80064a0a 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,126 @@ -
- Opex -
- -# Opex Core +

+
+ Opex +
+

- - Opex is released under the MIT license. - - - PRs welcome! - - - Last commit - - - - +Core is a Kotlin based cryptocurrency exchange and matching engine from the OPEX project. This extendable and +microservice project work as a vanilla core for running cryptocurrency exchanges.

-**OPEX** Core is a Kotlin based cryptocurrency exchange and matching engine from the OPEX project. This extendable and -microservice architectured project work as a vanilla core for running cryptocurrency exchanges. +

+ + Opex is released under the MIT license. + + + PRs welcome! + + + Last commit + + + + + + + +

## Contents -- [Install](#Install) +- [Build and Run](#build-and-run) +- [Live Demo](#live-demo) - [Architecture Overview](#overview) -- [Demo](#demo) -- [Documentation](#documentation) - [How to Contribute](#how-to-contribute) - [License](#license) -- [Additional Info](#info) -## Install +## Build and Run You need to have [Maven](https://maven.apache.org) and [Docker](https://www.docker.com) installed. -1. Clone this repository or [download the latest zip](https://github.com/opexdev/Back-end). -2. Build each module using `mvn clean install` command. -3. Change directory to `./Deployment` and build docker containers using `docker-compose build`. -4. In `./Deployment` directory, run docker containers which you've built in previous step by - using `docker-compose up -d` and wait for modules to be up and running. -5. You can make sure each module is running correctly by typing `http://localhost:8500` to your browser and check module - health. -6. You can also make sure middlewares (kafka, consule, etc) are running correctly by using `docker ps`. - -## Architecture Overview - -
- Opex -
+1. Clone the repository `git clone https://github.com/opexdev/core.git` +1. Run `cd core` +1. Run `mvn clean install` command. +1. Run `docker-compose up --build`. +1. Run `docker ps` to see if every service is running. -## Demo +## Live Demo -Check out Opex [demo][WebDemo]. +Deployed at [demo.opex.dev](https://demo.opex.dev). -[WebDemo]: https://opex.dev/demo - -## Documentation - -The full documentation for Opex can be found on our [website][docs]. +## Architecture Overview -[docs]: https://opex.dev +```mermaid + graph LR + USER_MANAGMENT(User Management) + KAFKA(Kafka) + ZOOKEEPER(Zookeeper) + REDIS[(Redis)] + ACCOUNTANT_POSTGRESQL[(PSQL)] + REFERRAL_POSTGRESQL[(PSQL)] + USER_MANAGMENT_POSTGRESQL[(PSQL)] + WALLET_POSTGRESQL[(PSQL)] + BC_GATEWAY_POSTGRESQL[(PSQL)] + EVENTLOG_POSTGRESQL[(PSQL)] + ACCOUNTANT(Accountant) + API(API) + WALLET(Wallet) + MATCHING_ENGINE(Matching Engine) + MATCHING_GATEWAY(Matching Gateway) + REFERRAL(Referral) + STORAGE(Storage) + BC_GATEWAY(Blockchain Gateway) + WEBSOCKET(Websocket) + ADMIN(Admin) + CAPTCHA(Captcha) + EVENTLOG(Event Log) + + API-->MATCHING_GATEWAY + API-->WALLET + API-->REFERRAL + API-->STORAGE + API-->BC_GATEWAY + API-->ACCOUNTANT + + MATCHING_ENGINE-->REDIS + USER_MANAGMENT-->USER_MANAGMENT_POSTGRESQL + BC_GATEWAY-->BC_GATEWAY_POSTGRESQL + REFERRAL-->REFERRAL_POSTGRESQL + WALLET-->WALLET_POSTGRESQL + ACCOUNTANT-->ACCOUNTANT_POSTGRESQL + EVENTLOG-->EVENTLOG_POSTGRESQL + + subgraph MESSAGING + KAFKA + ZOOKEEPER + end + + subgraph MATCHING DOMAIN + MATCHING_GATEWAY-->MATCHING_ENGINE + end + + subgraph ACCOUNTANT DOMAIN + ACCOUNTANT-->WALLET + REFERRAL-->WALLET + end + + subgraph DATA STORE + BC_GATEWAY_POSTGRESQL + REFERRAL_POSTGRESQL + ACCOUNTANT_POSTGRESQL + WALLET_POSTGRESQL + USER_MANAGMENT_POSTGRESQL + EVENTLOG_POSTGRESQL + REDIS + end +``` ## How to Contribute We want to make contributing to this project as easy and transparent as possible, and we are grateful to the developer -for contributing bug fixes and improvements. Read our contribution docutmentation [here][contribute]. - -[contribute]: https://opex.dev +for contributing bug fixes and improvements. ## License -Opex is MIT licensed, as found in the [LICENSE][l] file. - -[l]: https://github.com/opexdev/Back-end/blob/main/LICENSE - -## Additional info - -For any other questions, feel free to contact us at [hi@opex.dev](hi@opex.dev). - +OPEX is [MIT licensed](https://github.com/opexdev/core/blob/main/LICENSE). diff --git a/accountant/accountant-app/Dockerfile b/accountant/accountant-app/Dockerfile index 7c71f9447..268392261 100644 --- a/accountant/accountant-app/Dockerfile +++ b/accountant/accountant-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/accountant/accountant-app/pom.xml b/accountant/accountant-app/pom.xml index 7f6034024..835465fa0 100644 --- a/accountant/accountant-app/pom.xml +++ b/accountant/accountant-app/pom.xml @@ -59,6 +59,14 @@ org.springframework.cloud spring-cloud-starter-vault-config + + org.springframework.boot + spring-boot-starter-actuator + + + co.nilin.opex.utility.preferences + preferences + diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt index d8a8791c2..05eb0ba44 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt @@ -1,141 +1,135 @@ -package co.nilin.opex.accountant.app.config - -import co.nilin.opex.accountant.app.listener.AccountantEventListener -import co.nilin.opex.accountant.app.listener.AccountantTempEventListener -import co.nilin.opex.accountant.app.listener.AccountantTradeListener -import co.nilin.opex.accountant.app.listener.OrderListener -import co.nilin.opex.accountant.core.api.FinancialActionJobManager -import co.nilin.opex.accountant.core.api.OrderManager -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.core.service.FinancialActionJobManagerImpl -import co.nilin.opex.accountant.core.service.OrderManagerImpl -import co.nilin.opex.accountant.core.service.TradeManagerImpl -import co.nilin.opex.accountant.core.spi.* -import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener -import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener -import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener -import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.scheduling.annotation.EnableScheduling - -@Configuration -@EnableScheduling -class AppConfig { - - @Bean - fun getFinancialActionJobManager( - financialActionLoader: FinancialActionLoader, - financialActionPersister: FinancialActionPersister, - walletProxy: WalletProxy - ): FinancialActionJobManager { - return FinancialActionJobManagerImpl( - financialActionLoader, - financialActionPersister, - walletProxy - ) - } - - @Bean - fun orderManager( - pairConfigLoader: PairConfigLoader, - financialActionPersister: FinancialActionPersister, - financeActionLoader: FinancialActionLoader, - orderPersister: OrderPersister, - tempEventPersister: TempEventPersister, - tempEventRepublisher: TempEventRepublisher, - richOrderPublisher: RichOrderPublisher - ): OrderManager { - return OrderManagerImpl( - pairConfigLoader, - financialActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - tempEventRepublisher, - richOrderPublisher - ) - } - - @Bean - fun tradeManager( - pairStaticRateLoader: PairStaticRateLoader, - financeActionPersister: FinancialActionPersister, - financeActionLoader: FinancialActionLoader, - orderPersister: OrderPersister, - tempEventPersister: TempEventPersister, - richTradePublisher: RichTradePublisher, - richOrderPublisher: RichOrderPublisher, - walletProxy: WalletProxy, - @Value("\${app.coin}") platformCoin: String, - @Value("\${app.address}") platformAddress: String - ): TradeManager { - return TradeManagerImpl( - pairStaticRateLoader, - financeActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - richTradePublisher, - richOrderPublisher, - walletProxy, - platformCoin, - platformAddress - ) - } - - @Bean - fun orderListener(orderManager: OrderManager): OrderListener { - return OrderListener(orderManager) - } - - @Bean - fun accountantTradeListener(tradeManager: TradeManager): AccountantTradeListener { - return AccountantTradeListener(tradeManager) - } - - @Bean - fun accountantEventListener(orderManager: OrderManager): AccountantEventListener { - return AccountantEventListener(orderManager) - } - - @Bean - fun accountantTempEventListener( - orderManager: OrderManager, - tradeManager: TradeManager - ): AccountantTempEventListener { - return AccountantTempEventListener(orderManager, tradeManager) - } - - @Autowired - fun configureOrderListener(orderKafkaListener: OrderKafkaListener, orderListener: OrderListener) { - orderKafkaListener.addOrderListener(orderListener) - } - - @Autowired - fun configureTradeListener( - tradeKafkaListener: TradeKafkaListener, - accountantTradeListener: AccountantTradeListener - ) { - tradeKafkaListener.addTradeListener(accountantTradeListener) - } - - @Autowired - fun configureEventListener( - eventKafkaListener: EventKafkaListener, - accountantEventListener: AccountantEventListener - ) { - eventKafkaListener.addEventListener(accountantEventListener) - } - - @Autowired - fun configureTempEventListener( - tempEventKafkaListener: TempEventKafkaListener, - accountantTempEventListener: AccountantTempEventListener - ) { - tempEventKafkaListener.addEventListener(accountantTempEventListener) - } - +package co.nilin.opex.accountant.app.config + +import co.nilin.opex.accountant.app.listener.AccountantEventListener +import co.nilin.opex.accountant.app.listener.AccountantTempEventListener +import co.nilin.opex.accountant.app.listener.AccountantTradeListener +import co.nilin.opex.accountant.app.listener.OrderListener +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.api.FinancialActionJobManager +import co.nilin.opex.accountant.core.api.OrderManager +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.core.service.FinancialActionJobManagerImpl +import co.nilin.opex.accountant.core.service.OrderManagerImpl +import co.nilin.opex.accountant.core.service.TradeManagerImpl +import co.nilin.opex.accountant.core.spi.* +import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableScheduling + +@Configuration +@EnableScheduling +class AppConfig { + + @Bean + fun getFinancialActionJobManager( + financialActionLoader: FinancialActionLoader, + financialActionPersister: FinancialActionPersister, + walletProxy: WalletProxy + ): FinancialActionJobManager { + return FinancialActionJobManagerImpl( + financialActionLoader, + financialActionPersister, + walletProxy + ) + } + + @Bean + fun orderManager( + pairConfigLoader: PairConfigLoader, + financialActionPersister: FinancialActionPersister, + financeActionLoader: FinancialActionLoader, + orderPersister: OrderPersister, + tempEventPersister: TempEventPersister, + tempEventRepublisher: TempEventRepublisher, + richOrderPublisher: RichOrderPublisher + ): OrderManager { + return OrderManagerImpl( + pairConfigLoader, + financialActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + tempEventRepublisher, + richOrderPublisher + ) + } + + @Bean + fun tradeManager( + financeActionPersister: FinancialActionPersister, + financeActionLoader: FinancialActionLoader, + orderPersister: OrderPersister, + tempEventPersister: TempEventPersister, + richTradePublisher: RichTradePublisher, + richOrderPublisher: RichOrderPublisher, + feeCalculator: FeeCalculator, + ): TradeManager { + return TradeManagerImpl( + financeActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + richTradePublisher, + richOrderPublisher, + feeCalculator + ) + } + + @Bean + fun orderListener(orderManager: OrderManager): OrderListener { + return OrderListener(orderManager) + } + + @Bean + fun accountantTradeListener(tradeManager: TradeManager): AccountantTradeListener { + return AccountantTradeListener(tradeManager) + } + + @Bean + fun accountantEventListener(orderManager: OrderManager): AccountantEventListener { + return AccountantEventListener(orderManager) + } + + @Bean + fun accountantTempEventListener( + orderManager: OrderManager, + tradeManager: TradeManager + ): AccountantTempEventListener { + return AccountantTempEventListener(orderManager, tradeManager) + } + + @Autowired + fun configureOrderListener(orderKafkaListener: OrderKafkaListener, orderListener: OrderListener) { + orderKafkaListener.addOrderListener(orderListener) + } + + @Autowired + fun configureTradeListener( + tradeKafkaListener: TradeKafkaListener, + accountantTradeListener: AccountantTradeListener + ) { + tradeKafkaListener.addTradeListener(accountantTradeListener) + } + + @Autowired + fun configureEventListener( + eventKafkaListener: EventKafkaListener, + accountantEventListener: AccountantEventListener + ) { + eventKafkaListener.addEventListener(accountantEventListener) + } + + @Autowired + fun configureTempEventListener( + tempEventKafkaListener: TempEventKafkaListener, + accountantTempEventListener: AccountantTempEventListener + ) { + tempEventKafkaListener.addEventListener(accountantTempEventListener) + } + } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt new file mode 100644 index 000000000..d0767d767 --- /dev/null +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt @@ -0,0 +1,54 @@ +package co.nilin.opex.accountant.app.config + +import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository +import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository +import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel +import co.nilin.opex.utility.preferences.Preferences +import kotlinx.coroutines.reactor.awaitSingleOrNull +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.DependsOn +import org.springframework.stereotype.Component +import javax.annotation.PostConstruct + +@Component +@DependsOn("postgresConfig") +class InitializeService( + private val pairConfigRepository: PairConfigRepository, + private val pairFeeConfigRepository: PairFeeConfigRepository +) { + @Autowired + private lateinit var preferences: Preferences + + @PostConstruct + fun init() = runBlocking { + preferences.markets.map { + val pair = it.pair ?: "${it.leftSide}_${it.rightSide}" + val leftSideCurrency = preferences.currencies.first { c -> it.leftSide == c.symbol } + val rightSideCurrency = preferences.currencies.first { c -> it.rightSide == c.symbol } + val leftSideFraction = (it.leftSideFraction ?: leftSideCurrency.precision).toDouble() + val rightSideFraction = (it.rightSideFraction ?: rightSideCurrency.precision).toDouble() + pairConfigRepository.insert( + pair, + it.leftSide, + it.rightSide, + leftSideFraction, + rightSideFraction + ).awaitSingleOrNull() + it.feeConfigs.forEach { f -> + runCatching { + pairFeeConfigRepository.save( + PairFeeConfigModel( + null, + pair, + f.direction, + f.userLevel, + f.makerFee.toDouble(), + f.takerFee.toDouble() + ) + ).awaitSingleOrNull() + } + } + } + } +} diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt index 3ce8cc282..2fefebc77 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt @@ -1,51 +1,60 @@ -package co.nilin.opex.accountant.app.controller - -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.accountant.core.spi.FinancialActionLoader -import co.nilin.opex.accountant.core.spi.PairConfigLoader -import co.nilin.opex.accountant.core.spi.WalletProxy -import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent -import co.nilin.opex.matching.engine.core.model.OrderDirection -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RestController -import java.math.BigDecimal - -@RestController -class AccountantController( - val walletProxy: WalletProxy, - val financialActionLoader: FinancialActionLoader, - val pairConfigLoader: PairConfigLoader -) { - data class BooleanResponse(val result: Boolean) - - @GetMapping("{uuid}/create_order/{amount}_{currency}/allowed") - suspend fun canCreateOrder( - @PathVariable("uuid") uuid: String, - @PathVariable("currency") currency: String, - @PathVariable("amount") amount: BigDecimal - ): BooleanResponse { - return BooleanResponse( - financialActionLoader.countUnprocessed(uuid, currency, SubmitOrderEvent::class.simpleName!!) <= 0 - && walletProxy.canFulfil(currency, "main", uuid, amount) - ) - } - - @GetMapping( - value = ["/config/{pair}/fee/{direction}-{userLevel}", "/config/{pair}/fee/{direction}"] - ) - suspend fun fetchPairFeeConfig( - @PathVariable("pair") pair: String, - @PathVariable("direction") direction: OrderDirection, - @PathVariable("userLevel") level: String? - ): PairFeeConfig { - return pairConfigLoader.load(pair, direction, level ?: "") - } - - @GetMapping("/config/all") - suspend fun fetchPairConfigs(): List { - return pairConfigLoader.loadPairConfigs() - } - +package co.nilin.opex.accountant.app.controller + +import co.nilin.opex.accountant.app.data.PairFeeResponse +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.FinancialActionLoader +import co.nilin.opex.accountant.core.spi.PairConfigLoader +import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse +import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent +import co.nilin.opex.matching.engine.core.model.OrderDirection +import org.slf4j.LoggerFactory +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController +import java.math.BigDecimal + +@RestController +class AccountantController( + val walletProxy: WalletProxy, + val financialActionLoader: FinancialActionLoader, + val pairConfigLoader: PairConfigLoader +) { + + private val logger = LoggerFactory.getLogger(AccountantController::class.java) + + @GetMapping("{uuid}/create_order/{amount}_{currency}/allowed") + suspend fun canCreateOrder( + @PathVariable("uuid") uuid: String, + @PathVariable("currency") currency: String, + @PathVariable("amount") amount: BigDecimal + ): BooleanResponse { + val canFulfil = runCatching { walletProxy.canFulfil(currency, "main", uuid, amount) } + .onFailure { logger.error(it.message) } + .getOrElse { false } + val unprocessed = financialActionLoader.countUnprocessed(uuid, currency, SubmitOrderEvent::class.simpleName!!) + return BooleanResponse(unprocessed <= 0 && canFulfil) + } + + @GetMapping(value = ["/config/{pair}/fee/{direction}-{userLevel}", "/config/{pair}/fee/{direction}"]) + suspend fun fetchPairFeeConfig( + @PathVariable("pair") pair: String, + @PathVariable("direction") direction: OrderDirection, + @PathVariable("userLevel") level: String? + ): PairFeeConfig { + return pairConfigLoader.load(pair, direction, level ?: "") + } + + @GetMapping("/config/all") + suspend fun fetchPairConfigs(): List { + return pairConfigLoader.loadPairConfigs() + } + + @GetMapping("/config/fee/all") + suspend fun fetchFeeConfigs(): List { + return pairConfigLoader.loadPairFeeConfigs() + .map { PairFeeResponse(it.pairConfig.pair, it.direction, it.userLevel, it.makerFee, it.takerFee) } + } + } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt new file mode 100644 index 000000000..fa6accf6d --- /dev/null +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.accountant.app.data + +data class PairFeeResponse( + val pair:String, + val direction: String, + val userLevel: String, + val makerFee: Double, + val takerFee: Double +) \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt index 829e6c88d..605798a36 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt @@ -1,19 +1,20 @@ -package co.nilin.opex.accountant.app.listener - -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.ports.kafka.listener.spi.TradeListener -import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent -import kotlinx.coroutines.runBlocking - -class AccountantTradeListener(private val tradeManager: TradeManager) : TradeListener { - - override fun id(): String { - return "TradeListener" - } - - override fun onTrade(tradeEvent: TradeEvent, partition: Int, offset: Long, timestamp: Long) { - runBlocking { - tradeManager.handleTrade(tradeEvent) - } - } +package co.nilin.opex.accountant.app.listener + +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.ports.kafka.listener.spi.TradeListener +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking + +class AccountantTradeListener(private val tradeManager: TradeManager) : TradeListener { + + override fun id(): String { + return "TradeListener" + } + + override fun onTrade(tradeEvent: TradeEvent, partition: Int, offset: Long, timestamp: Long) { + runBlocking { + tradeManager.handleTrade(tradeEvent) + } + } } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt index 16278a928..ac0286ad4 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt @@ -5,15 +5,20 @@ import co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequest import co.nilin.opex.accountant.ports.kafka.listener.spi.OrderSubmitRequestListener import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory class OrderListener(private val orderManager: OrderManager) : OrderSubmitRequestListener { + private val logger = LoggerFactory.getLogger(OrderListener::class.java) + override fun id(): String { return "OrderListener" } override fun onOrder(order: OrderSubmitRequest, partition: Int, offset: Long, timestamp: Long) { runBlocking { + logger.info("Order submit event received ${order.ouid}") + orderManager.handleRequestOrder( SubmitOrderEvent( order.ouid, diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt index b8b1a73db..dc7699b13 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt @@ -1,30 +1,29 @@ -package co.nilin.opex.accountant.app.scheduler - -import co.nilin.opex.accountant.core.api.FinancialActionJobManager -import kotlinx.coroutines.runBlocking -import org.slf4j.LoggerFactory -import org.springframework.context.annotation.Profile -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Service - -@Service -@Profile("scheduled") -class FinancialActionsJob( - val financialActionJobManager: FinancialActionJobManager -) { - - private val log = LoggerFactory.getLogger(FinancialActionsJob::class.java) - - @Scheduled(fixedDelay = 10000) - fun processFinancialActions() { - runBlocking { - try { - //read unprocessed fa records and call transfer - financialActionJobManager.processFinancialActions(0, 100) - } catch (e: Exception) { - log.error("Job error!", e) - } - } - } - +package co.nilin.opex.accountant.app.scheduler + +import co.nilin.opex.accountant.core.api.FinancialActionJobManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +@Profile("scheduled") +class FinancialActionsJob(val financialActionJobManager: FinancialActionJobManager) { + + private val log = LoggerFactory.getLogger(FinancialActionsJob::class.java) + + @Scheduled(fixedDelay = 10000) + fun processFinancialActions() { + runBlocking(Dispatchers.IO) { + try { + //read unprocessed fa records and call transfer + financialActionJobManager.processFinancialActions(0, 100) + } catch (e: Exception) { + log.error("Job error!", e) + } + } + } + } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/resources/application.yml b/accountant/accountant-app/src/main/resources/application.yml index 6e5e58484..028a81a7c 100644 --- a/accountant/accountant-app/src/main/resources/application.yml +++ b/accountant/accountant-app/src/main/resources/application.yml @@ -45,7 +45,6 @@ spring: config: import: vault://secret/${spring.application.name} app: - coin: nln address: 1 wallet: url: lb://opex-wallet/ diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt new file mode 100644 index 000000000..b650e29e6 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.accountant.core.api + +import co.nilin.opex.accountant.core.model.FeeFinancialActions +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent + +interface FeeCalculator { + + suspend fun createFeeActions( + trade: TradeEvent, + makerOrder: Order, + takerOrder: Order, + makerParentFA: FinancialAction?, + takerParentFA: FinancialAction? + ): FeeFinancialActions + +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt new file mode 100644 index 000000000..078496757 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.accountant.core.model + +data class FeeFinancialActions( + val makerFeeAction: FinancialAction, + val takerFeeAction: FinancialAction +) \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt index 0de5d8a81..5f812e09c 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt @@ -1,53 +1,28 @@ -package co.nilin.opex.accountant.core.model - -import java.math.BigDecimal -import java.time.LocalDateTime - -class FinancialAction( - val id: Long? = null, - val parent: FinancialAction?, - val eventType: String, - val pointer: String, - val symbol: String, - val amount: BigDecimal, - val sender: String, - val senderWalletType: String, - val receiver: String, - val receiverWalletType: String, - val createDate: LocalDateTime -) { - constructor( - parent: FinancialAction?, - eventType: String, - pointer: String, - symbol: String, - amount: BigDecimal, - sender: String, - senderWalletType: String, - receiver: String, - receiverWalletType: String, - createDate: LocalDateTime - ) : this( - null, - parent, - eventType, - pointer, - symbol, - amount, - sender, - senderWalletType, - receiver, - receiverWalletType, - createDate - ) - - override fun toString(): String { - return "FinancialAction(id=$id, parent=$parent, eventType='$eventType', pointer='$pointer', symbol='$symbol', amount=$amount, sender='$sender', senderWalletType='$senderWalletType', receiver='$receiver', receiverWalletType='$receiverWalletType', createDate=$createDate)" - } - - -} - -enum class FinancialActionStatus { - CREATED, PROCESSED, ERROR +package co.nilin.opex.accountant.core.model + +import java.math.BigDecimal +import java.time.LocalDateTime + +class FinancialAction( + val parent: FinancialAction?, + val eventType: String, + val pointer: String, + val symbol: String, + val amount: BigDecimal, + val sender: String, + val senderWalletType: String, + val receiver: String, + val receiverWalletType: String, + val createDate: LocalDateTime, + val retryCount: Int = 0, + val id: Long? = null +) { + + override fun toString(): String { + return "FinancialAction(id=$id, parent=$parent, eventType='$eventType', pointer='$pointer', symbol='$symbol', amount=$amount, sender='$sender', senderWalletType='$senderWalletType', receiver='$receiver', receiverWalletType='$receiverWalletType', createDate=$createDate)" + } +} + +enum class FinancialActionStatus { + CREATED, PROCESSED, ERROR } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt index 2298687b1..00498920b 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt @@ -1,31 +1,36 @@ -package co.nilin.opex.accountant.core.model - -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import java.math.BigDecimal - -data class Order( - val pair: String, - val ouid: String, - var matchingEngineId: Long?, - val makerFee: Double, - val takerFee: Double, - val leftSideFraction: Double, - val rightSideFraction: Double, - val uuid: String, - val userLevel: String, - val direction: OrderDirection, - val matchConstraint: MatchConstraint, - val orderType: OrderType, - val price: Long, - val quantity: Long, - val filledQuantity: Long, - val origPrice: BigDecimal, - val origQuantity: BigDecimal, - val filledOrigQuantity: BigDecimal, - val firstTransferAmount: BigDecimal, - var remainedTransferAmount: BigDecimal, - var status: Int, - val id: Long? = null -) \ No newline at end of file +package co.nilin.opex.accountant.core.model + +import co.nilin.opex.matching.engine.core.model.MatchConstraint +import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.matching.engine.core.model.OrderType +import java.math.BigDecimal + +data class Order( + val pair: String, + val ouid: String, + var matchingEngineId: Long?, + val makerFee: Double, + val takerFee: Double, + val leftSideFraction: Double, + val rightSideFraction: Double, + val uuid: String, + val userLevel: String, + val direction: OrderDirection, + val matchConstraint: MatchConstraint, + val orderType: OrderType, + val price: Long, + val quantity: Long, + val filledQuantity: Long, + val origPrice: BigDecimal, + val origQuantity: BigDecimal, + val filledOrigQuantity: BigDecimal, + val firstTransferAmount: BigDecimal, + var remainedTransferAmount: BigDecimal, + var status: Int, + val id: Long? = null +) { + + fun isAsk() = direction == OrderDirection.ASK + + fun isBid() = direction == OrderDirection.BID +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt new file mode 100644 index 000000000..e1ccfe5e4 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -0,0 +1,77 @@ +package co.nilin.opex.accountant.core.service + +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.model.FeeFinancialActions +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class FeeCalculatorImpl(@Value("\${app.address}") private val platformAddress: String) : FeeCalculator { + + private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java) + + override suspend fun createFeeActions( + trade: TradeEvent, + makerOrder: Order, + takerOrder: Order, + makerParentFA: FinancialAction?, + takerParentFA: FinancialAction? + ): FeeFinancialActions { + logger.info("Start fee calculation for trade ${trade.takerUuid}") + + val makerMatchedAmount = if (makerOrder.isAsk()) { + trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) + } else { + trade.matchedQuantity.toBigDecimal() + .multiply(makerOrder.leftSideFraction.toBigDecimal()) + .multiply(trade.makerPrice.toBigDecimal()) + .multiply(makerOrder.rightSideFraction.toBigDecimal()) + } + + val takerMatchedAmount = if (takerOrder.isAsk()) { + trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) + } else { + trade.matchedQuantity.toBigDecimal() + .multiply(takerOrder.leftSideFraction.toBigDecimal()) + .multiply(trade.makerPrice.toBigDecimal()) + .multiply(takerOrder.rightSideFraction.toBigDecimal()) + } + + //calculate maker fee + val makerFeeAction = FinancialAction( + makerParentFA, + TradeEvent::class.simpleName!!, + trade.takerOuid, + if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + takerMatchedAmount.multiply(makerOrder.makerFee.toBigDecimal()), + trade.makerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) + logger.info("trade event makerFeeAction $makerFeeAction") + + //calculate taker fee + val takerFeeAction = FinancialAction( + takerParentFA, + TradeEvent::class.simpleName!!, + trade.makerOuid, + if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + makerMatchedAmount.multiply(takerOrder.takerFee.toBigDecimal()), + trade.takerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) + logger.info("trade event takerFeeAction $takerFeeAction") + + return FeeFinancialActions(makerFeeAction, takerFeeAction) + } +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt index 7ebcc5ea5..764a7c4ce 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt @@ -13,26 +13,30 @@ class FinancialActionJobManagerImpl( private val walletProxy: WalletProxy ) : FinancialActionJobManager { + private val retryLimit = 10 private val log = LoggerFactory.getLogger(FinancialActionJobManagerImpl::class.java) override suspend fun processFinancialActions(offset: Long, size: Long) { val factions = financialActionLoader.loadUnprocessed(offset, size) - factions.forEach { faction -> + factions.forEach { try { walletProxy.transfer( - faction.symbol, - faction.senderWalletType, - faction.sender, - faction.receiverWalletType, - faction.receiver, - faction.amount, - faction.eventType + faction.pointer, - null + it.symbol, + it.senderWalletType, + it.sender, + it.receiverWalletType, + it.receiver, + it.amount, + it.eventType + it.pointer, + "fa${it.id}" ) - financialActionPersister.updateStatus(faction, FinancialActionStatus.PROCESSED) + financialActionPersister.updateStatus(it, FinancialActionStatus.PROCESSED) } catch (e: Exception) { log.error("financial job error", e) - financialActionPersister.updateStatus(faction, FinancialActionStatus.ERROR) + financialActionPersister.updateStatus( + it, + if (it.retryCount >= retryLimit) FinancialActionStatus.ERROR else FinancialActionStatus.CREATED + ) } } } diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt index 3527b2a7a..9b6f756bf 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt @@ -3,6 +3,7 @@ package co.nilin.opex.accountant.core.service import co.nilin.opex.accountant.core.api.OrderManager import co.nilin.opex.accountant.core.inout.OrderStatus import co.nilin.opex.accountant.core.inout.RichOrder +import co.nilin.opex.accountant.core.inout.RichOrderUpdate import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order import co.nilin.opex.accountant.core.spi.* @@ -41,25 +42,24 @@ open class OrderManagerImpl( amount for sell (ask): quantity amount for buy (bid): quantity * price */ - val financialAction = - FinancialAction( - null, - SubmitOrderEvent::class.simpleName!!, - submitOrderEvent.ouid, - symbol, - if (submitOrderEvent.direction == OrderDirection.ASK) { - BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) - } else { - BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) - .multiply(submitOrderEvent.price.toBigDecimal()) - .multiply(pairFeeConfig.pairConfig.rightSideFraction.toBigDecimal()) - }, - submitOrderEvent.uuid, - "main", - submitOrderEvent.uuid, - "exchange", - LocalDateTime.now() - ) + val financialAction = FinancialAction( + null, + SubmitOrderEvent::class.simpleName!!, + submitOrderEvent.ouid, + symbol, + if (submitOrderEvent.direction == OrderDirection.ASK) { + BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) + } else { + BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) + .multiply(submitOrderEvent.price.toBigDecimal()) + .multiply(pairFeeConfig.pairConfig.rightSideFraction.toBigDecimal()) + }, + submitOrderEvent.uuid, + "main", + submitOrderEvent.uuid, + "exchange", + LocalDateTime.now() + ) //store order (ouid, uuid, fees, userlevel, pair, direction, price, quantity, filledQ, status, transfered) orderPersister.save( Order( @@ -106,40 +106,6 @@ open class OrderManagerImpl( return emptyList() } - private suspend fun publishRichOrder(order: Order, remainedQuantity: BigDecimal, status: OrderStatus? = null) { - richOrderPublisher.publish( - RichOrder( - order.id, - order.pair, - order.ouid, - order.uuid, - order.userLevel, - order.makerFee.toBigDecimal(), - order.takerFee.toBigDecimal(), - order.leftSideFraction.toBigDecimal(), - order.rightSideFraction.toBigDecimal(), - order.direction, - order.matchConstraint, - order.orderType, - order.origPrice, - order.origQuantity, - order.origPrice.multiply(order.origQuantity), - order.quantity.toBigDecimal().subtract(remainedQuantity) - .multiply(order.leftSideFraction.toBigDecimal()), - order.origPrice.multiply( - order.quantity.toBigDecimal().subtract(remainedQuantity) - ), - status?.code ?: if (remainedQuantity.compareTo(BigDecimal.ZERO) == 0) { - OrderStatus.FILLED.code - } else if (remainedQuantity.compareTo(order.quantity.toBigDecimal()) == 0) { - OrderStatus.NEW.code - } else { - OrderStatus.PARTIALLY_FILLED.code - } - ) - ) - } - override suspend fun handleUpdateOrder(updatedOrderEvent: UpdatedOrderEvent): List { TODO("Not yet implemented") } @@ -176,7 +142,15 @@ open class OrderManagerImpl( //update order status order.status = OrderStatus.REJECTED.code orderPersister.save(order) - publishRichOrder(order, order.quantity.toBigDecimal(), OrderStatus.REJECTED) + richOrderPublisher.publish( + RichOrderUpdate( + order.ouid, + order.price.toBigDecimal(), + order.quantity.toBigDecimal(), + BigDecimal.ZERO, + OrderStatus.REJECTED + ) + ) return financialActionPersister.persist(listOf(financialAction)) } @@ -212,7 +186,49 @@ open class OrderManagerImpl( //update order status order.status = OrderStatus.CANCELED.code orderPersister.save(order) - publishRichOrder(order, cancelOrderEvent.quantity.toBigDecimal(), OrderStatus.CANCELED) + richOrderPublisher.publish( + RichOrderUpdate( + order.ouid, + order.price.toBigDecimal(), + order.quantity.toBigDecimal(), + cancelOrderEvent.remainedQuantity.toBigDecimal(), + OrderStatus.REJECTED + ) + ) return financialActionPersister.persist(listOf(financialAction)) } + + private suspend fun publishRichOrder(order: Order, remainedQuantity: BigDecimal, status: OrderStatus? = null) { + richOrderPublisher.publish( + RichOrder( + order.id, + order.pair, + order.ouid, + order.uuid, + order.userLevel, + order.makerFee.toBigDecimal(), + order.takerFee.toBigDecimal(), + order.leftSideFraction.toBigDecimal(), + order.rightSideFraction.toBigDecimal(), + order.direction, + order.matchConstraint, + order.orderType, + order.origPrice, + order.origQuantity, + order.origPrice.multiply(order.origQuantity), + order.quantity.toBigDecimal().subtract(remainedQuantity) + .multiply(order.leftSideFraction.toBigDecimal()), + order.origPrice.multiply( + order.quantity.toBigDecimal().subtract(remainedQuantity) + ), + status?.code ?: if (remainedQuantity.compareTo(BigDecimal.ZERO) == 0) { + OrderStatus.FILLED.code + } else if (remainedQuantity.compareTo(order.quantity.toBigDecimal()) == 0) { + OrderStatus.NEW.code + } else { + OrderStatus.PARTIALLY_FILLED.code + } + ) + ) + } } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt index 9f237d7ca..dfcf207e6 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt @@ -1,5 +1,6 @@ package co.nilin.opex.accountant.core.service +import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.api.TradeManager import co.nilin.opex.accountant.core.inout.OrderStatus import co.nilin.opex.accountant.core.inout.RichOrderUpdate @@ -8,23 +9,19 @@ import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order import co.nilin.opex.accountant.core.spi.* import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent -import co.nilin.opex.matching.engine.core.model.OrderDirection import org.slf4j.LoggerFactory import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal import java.time.LocalDateTime open class TradeManagerImpl( - private val pairStaticRateLoader: PairStaticRateLoader, private val financeActionPersister: FinancialActionPersister, private val financeActionLoader: FinancialActionLoader, private val orderPersister: OrderPersister, private val tempEventPersister: TempEventPersister, private val richTradePublisher: RichTradePublisher, private val richOrderPublisher: RichOrderPublisher, - private val walletProxy: WalletProxy, - private val platformCoin: String, - private val platformAddress: String + private val feeCalculator: FeeCalculator ) : TradeManager { private val log = LoggerFactory.getLogger(TradeManagerImpl::class.java) @@ -46,39 +43,22 @@ open class TradeManagerImpl( } return emptyList() } - //check taker uuid - // - //check maker uuid - // - val leftSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.leftSideName) - val rightSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.rightSideName) - val takerMatchedAmount = if (takerOrder.direction == OrderDirection.ASK) { + val takerMatchedAmount = if (takerOrder.isAsk()) { trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) } else { trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) .multiply(trade.makerPrice.toBigDecimal()).multiply(takerOrder.rightSideFraction.toBigDecimal()) } - val takerPCFeeCoefficient: Double = if (takerOrder.direction == OrderDirection.ASK) { - leftSidePCRate ?: 0.0 - } else { - rightSidePCRate ?: 0.0 - } - - val makerMatchedAmount = if (makerOrder.direction == OrderDirection.ASK) { + val makerMatchedAmount = if (makerOrder.isAsk()) { trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) } else { trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) .multiply(trade.makerPrice.toBigDecimal()).multiply(makerOrder.rightSideFraction.toBigDecimal()) } + log.info("trade event configs loaded") - val makerPCFeeCoefficient: Double = if (makerOrder.direction == OrderDirection.ASK) { - leftSidePCRate ?: 0.0 - } else { - rightSidePCRate ?: 0.0 - } - log.info("trade event configs loaded ") //lookup for taker parent fa val takerParentFinancialAction = financeActionLoader.findLast(trade.takerUuid, trade.takerOuid) log.info("trade event takerParentFinancialAction {} ", takerParentFinancialAction) @@ -86,13 +66,6 @@ open class TradeManagerImpl( val makerParentFinancialAction = financeActionLoader.findLast(trade.makerUuid, trade.makerOuid) log.info("trade event makerParentFinancialAction {} ", makerParentFinancialAction) - - //calculate maker fee - val makerFee = makerOrder.makerFee - val makerTotalFeeWithPlatformCoin = takerMatchedAmount - .multiply(makerFee.toBigDecimal()) - .multiply(makerPCFeeCoefficient.toBigDecimal()) - //check if maker uuid can pay the fee with platform coin //create fa for transfer taker uuid symbol exchange wallet to maker symbol main wallet /* amount for sell (ask): match_quantity (if not pay by platform coin then - maker fee) @@ -102,11 +75,7 @@ open class TradeManagerImpl( takerParentFinancialAction, TradeEvent::class.simpleName!!, trade.takerOuid, - if (takerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, + if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, takerMatchedAmount, trade.takerUuid, "exchange", @@ -116,41 +85,7 @@ open class TradeManagerImpl( ) log.info("trade event takerTransferAction {}", takerTransferAction) financialActions.add(takerTransferAction) - val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && - walletProxy.canFulfil(platformCoin, "main", trade.makerUuid, makerTotalFeeWithPlatformCoin) - ) { - FinancialAction( - makerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.takerOuid, - platformCoin, - makerTotalFeeWithPlatformCoin, - trade.makerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } else { - FinancialAction( - makerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.takerOuid, - if (takerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - takerMatchedAmount.multiply(makerFee.toBigDecimal()), - trade.makerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } - log.info("trade event makerFeeAction {}", makerFeeAction) - financialActions.add(makerFeeAction) + //update taker order status takerOrder.remainedTransferAmount -= takerMatchedAmount if (takerOrder.filledQuantity == takerOrder.quantity) { @@ -160,13 +95,7 @@ open class TradeManagerImpl( log.info("taker order saved {}", takerOrder) publishTakerRichOrderUpdate(takerOrder, trade) - //calculate taker fee - val takerFee = takerOrder.takerFee - val takerTotalFeeWithPlatformCoin = takerMatchedAmount - .multiply(takerFee.toBigDecimal()) - .multiply(takerPCFeeCoefficient.toBigDecimal()) - - //create fa for transfer makeruuid symbol exchange wallet to taker symbol main wallet + //create fa for transfer makerUuid symbol exchange wallet to taker symbol main wallet /* amount for sell (ask): match_quantity (if not pay by platform coin then - taker fee) amount for buy (bid): match_quantity * maker price (if not pay by platform coin then - taker fee) @@ -175,11 +104,7 @@ open class TradeManagerImpl( makerParentFinancialAction, TradeEvent::class.simpleName!!, trade.makerOuid, - if (makerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, + if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, makerMatchedAmount, trade.makerUuid, "exchange", @@ -189,47 +114,7 @@ open class TradeManagerImpl( ) log.info("trade event makerTransferAction {}", makerTransferAction) financialActions.add(makerTransferAction) - //check if taker uuid can pay the fee with platform coin - val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && - walletProxy.canFulfil(platformCoin, "main", trade.takerUuid, takerTotalFeeWithPlatformCoin) - ) { - FinancialAction( - takerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - takerTotalFeeWithPlatformCoin, - trade.takerUuid, - "main", - platformAddress, - "", - LocalDateTime.now() - ) - } else { - FinancialAction( - takerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - makerMatchedAmount.multiply(takerFee.toBigDecimal()), - trade.takerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } - log.info("trade event takerFeeAction {}", takerFeeAction) - financialActions.add(takerFeeAction) //update maker order status makerOrder.remainedTransferAmount -= makerMatchedAmount if (makerOrder.filledQuantity == makerOrder.quantity) { @@ -239,6 +124,17 @@ open class TradeManagerImpl( log.info("maker order saved {}", makerOrder) publishMakerRichOrderUpdate(makerOrder, trade) + val feeActions = feeCalculator.createFeeActions( + trade, + makerOrder, + takerOrder, + makerParentFinancialAction, + takerParentFinancialAction + ).apply { + financialActions.add(makerFeeAction) + financialActions.add(takerFeeAction) + } + richTradePublisher.publish( RichTrade( trade.tradeId, @@ -251,8 +147,8 @@ open class TradeManagerImpl( takerOrder.origQuantity, takerOrder.origPrice.multiply(takerOrder.origQuantity), trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()), - takerFeeAction.amount, - takerFeeAction.symbol, + feeActions.takerFeeAction.amount, + feeActions.takerFeeAction.symbol, trade.makerOuid, trade.makerUuid, trade.makerOrderId, @@ -261,12 +157,11 @@ open class TradeManagerImpl( makerOrder.origQuantity, makerOrder.origPrice.multiply(makerOrder.origQuantity), trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), - makerFeeAction.amount, - makerFeeAction.symbol, + feeActions.makerFeeAction.amount, + feeActions.makerFeeAction.symbol, trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), trade.eventDate ) - ) return financeActionPersister.persist(financialActions) } diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt index f51e4eef9..5a3d54c69 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt @@ -1,9 +1,11 @@ -package co.nilin.opex.accountant.core.spi - -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionStatus - -interface FinancialActionPersister { - suspend fun persist(financialActions: List): List - suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) +package co.nilin.opex.accountant.core.spi + +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionStatus + +interface FinancialActionPersister { + + suspend fun persist(financialActions: List): List + + suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt index f7414fd3a..b4909f53f 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt @@ -1,12 +1,14 @@ -package co.nilin.opex.accountant.core.spi - -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.matching.engine.core.model.OrderDirection - -interface PairConfigLoader { - - suspend fun loadPairConfigs(): List - - suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig +package co.nilin.opex.accountant.core.spi + +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.matching.engine.core.model.OrderDirection + +interface PairConfigLoader { + + suspend fun loadPairConfigs(): List + + suspend fun loadPairFeeConfigs(): List + + suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig } \ No newline at end of file diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt index e9d638ece..c03c25330 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt @@ -1,267 +1,261 @@ -package co.nilin.opex.accountant.core.service - -import co.nilin.opex.accountant.core.api.OrderManager -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.Order -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.accountant.core.spi.* -import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent -import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import co.nilin.opex.matching.engine.core.model.Pair -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.MockitoAnnotations - -internal class TradeManagerImplTest() { - @Mock - lateinit var financialActionPersister: FinancialActionPersister - - @Mock - lateinit var financeActionLoader: FinancialActionLoader - - @Mock - lateinit var orderPersister: OrderPersister - - @Mock - lateinit var pairConfigLoader: PairConfigLoader - - @Mock - lateinit var pairStaticRateLoader: PairStaticRateLoader - - @Mock - lateinit var walletProxy: WalletProxy - - @Mock - lateinit var tempEventPersister: TempEventPersister - - @Mock - lateinit var tempEventRepublisher: TempEventRepublisher - - @Mock - lateinit var richOrderPublisher: RichOrderPublisher - - @Mock - lateinit var richTradePublisher: RichTradePublisher - - val orderManager: OrderManager - - val tradeManager: TradeManager - - - init { - MockitoAnnotations.openMocks(this) - orderManager = OrderManagerImpl( - pairConfigLoader, - financialActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - tempEventRepublisher, - richOrderPublisher - ) - tradeManager = TradeManagerImpl( - pairStaticRateLoader, - financialActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - richTradePublisher, - richOrderPublisher, - walletProxy, - "pcoin", - "0x0" - ) - runBlocking { - Mockito.`when`(tempEventPersister.loadTempEvents(ArgumentMatchers.anyString())).thenReturn(emptyList()) - } - } - - @Test - fun givenSellOrder_WhenMatchBuyOrderCome_thenFAMatched() { - runBlocking { - //given - val pair = Pair("eth", "btc") - val pairConfig = PairConfig( - pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.01 - ) - val makerSubmitOrderEvent = SubmitOrderEvent( - "mouid", - "muuid", - null, - pair, - 60000, - 1, - 0, - OrderDirection.ASK, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) - - val takerSubmitOrderEvent = SubmitOrderEvent( - "touid", - "tuuid", - null, - pair, - 70000, - 1, - 0, - OrderDirection.BID, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) - - val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) - //when - val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) - - Assertions.assertEquals(4, tradeFinancialActions.size) - Assertions.assertEquals( - (makerSubmitOrderEvent.price * pairConfig.rightSideFraction), - tradeFinancialActions[0].amount.toDouble() - ) - } - } - - @Test - fun givenBuyOrder_WhenMatchSellOrderCome_thenFAMatched() { - runBlocking { - //given - val pair = Pair("eth", "btc") - val pairConfig = PairConfig( - pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.001 - ) - val makerSubmitOrderEvent = SubmitOrderEvent( - "mouid", - "muuid", - null, - pair, - 70000, - 1, - 0, - OrderDirection.BID, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) - - val takerSubmitOrderEvent = SubmitOrderEvent( - "touid", - "tuuid", - null, - pair, - 60000, - 1, - 0, - OrderDirection.ASK, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) - - val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) - //when - val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) - - Assertions.assertEquals(4, tradeFinancialActions.size) - Assertions.assertEquals( - makerSubmitOrderEvent.price * pairConfig.rightSideFraction, - tradeFinancialActions[2].amount.toDouble() - ) - } - } - - private fun makeTradeEvent( - pair: Pair, - takerSubmitOrderEvent: SubmitOrderEvent, - makerSubmitOrderEvent: SubmitOrderEvent - ): TradeEvent { - val tradeEvent = TradeEvent( - 0, - pair, - takerSubmitOrderEvent.ouid, - takerSubmitOrderEvent.uuid, - takerSubmitOrderEvent.orderId ?: -1, - takerSubmitOrderEvent.direction, - takerSubmitOrderEvent.price, - 0, - makerSubmitOrderEvent.ouid, - makerSubmitOrderEvent.uuid, - makerSubmitOrderEvent.orderId ?: 1, - makerSubmitOrderEvent.direction, - makerSubmitOrderEvent.price, - makerSubmitOrderEvent.quantity - takerSubmitOrderEvent.quantity, - takerSubmitOrderEvent.quantity - ) - return tradeEvent - } - - private fun prepareOrder( - pair: Pair, - pairConfig: PairConfig, - submitOrderEvent: SubmitOrderEvent, - makerFee: Double, - takerFee: Double - ) { - runBlocking { - Mockito.`when`(pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "")) - .thenReturn( - PairFeeConfig( - pairConfig, - submitOrderEvent.direction.toString(), - "", - makerFee, - takerFee - ) - ) - Mockito.`when`(financialActionPersister.persist(MockitoHelper.anyObject())) - .then { - return@then it.getArgument>(0) - } - - val financialActions = orderManager.handleRequestOrder(submitOrderEvent) - - val orderPairFeeConfig = - pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "") - val orderMakerFee = orderPairFeeConfig.makerFee * 1 //user level formula - val orderTakerFee = orderPairFeeConfig.takerFee * 1 //user level formula - Mockito.`when`(orderPersister.load(submitOrderEvent.ouid)).thenReturn( - Order( - submitOrderEvent.pair.toString(), - submitOrderEvent.ouid, - null, - orderMakerFee, - orderTakerFee, - orderPairFeeConfig.pairConfig.leftSideFraction, - orderPairFeeConfig.pairConfig.rightSideFraction, - submitOrderEvent.uuid, - "", - submitOrderEvent.direction, - submitOrderEvent.matchConstraint, - submitOrderEvent.orderType, - submitOrderEvent.price, - submitOrderEvent.quantity, - submitOrderEvent.quantity - submitOrderEvent.remainedQuantity, - submitOrderEvent.price.toBigDecimal(), - submitOrderEvent.quantity.toBigDecimal(), - (submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).toBigDecimal(), - financialActions[0].amount, - financialActions[0].amount, - 0 - ) - ) - } - } +package co.nilin.opex.accountant.core.service + +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.api.OrderManager +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.* +import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import co.nilin.opex.matching.engine.core.model.MatchConstraint +import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.matching.engine.core.model.OrderType +import co.nilin.opex.matching.engine.core.model.Pair +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +internal class TradeManagerImplTest { + + @Mock + lateinit var financialActionPersister: FinancialActionPersister + + @Mock + lateinit var financeActionLoader: FinancialActionLoader + + @Mock + lateinit var orderPersister: OrderPersister + + @Mock + lateinit var pairConfigLoader: PairConfigLoader + + @Mock + lateinit var tempEventPersister: TempEventPersister + + @Mock + lateinit var tempEventRepublisher: TempEventRepublisher + + @Mock + lateinit var richOrderPublisher: RichOrderPublisher + + @Mock + lateinit var richTradePublisher: RichTradePublisher + + private val orderManager: OrderManager + private val tradeManager: TradeManager + + init { + MockitoAnnotations.openMocks(this) + + orderManager = OrderManagerImpl( + pairConfigLoader, + financialActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + tempEventRepublisher, + richOrderPublisher + ) + + tradeManager = TradeManagerImpl( + financialActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + richTradePublisher, + richOrderPublisher, + FeeCalculatorImpl("0x0") + ) + + runBlocking { + Mockito.`when`(tempEventPersister.loadTempEvents(ArgumentMatchers.anyString())).thenReturn(emptyList()) + } + } + + @Test + fun givenSellOrder_WhenMatchBuyOrderCome_thenFAMatched() { + runBlocking { + //given + val pair = Pair("eth", "btc") + val pairConfig = PairConfig( + pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.01 + ) + val makerSubmitOrderEvent = SubmitOrderEvent( + "mouid", + "muuid", + null, + pair, + 60000, + 1, + 0, + OrderDirection.ASK, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) + + val takerSubmitOrderEvent = SubmitOrderEvent( + "touid", + "tuuid", + null, + pair, + 70000, + 1, + 0, + OrderDirection.BID, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) + + val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) + //when + val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) + + Assertions.assertEquals(4, tradeFinancialActions.size) + Assertions.assertEquals( + (makerSubmitOrderEvent.price * pairConfig.rightSideFraction), + tradeFinancialActions[0].amount.toDouble() + ) + } + } + + @Test + fun givenBuyOrder_WhenMatchSellOrderCome_thenFAMatched() { + runBlocking { + //given + val pair = Pair("eth", "btc") + val pairConfig = PairConfig( + pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.001 + ) + val makerSubmitOrderEvent = SubmitOrderEvent( + "mouid", + "muuid", + null, + pair, + 70000, + 1, + 0, + OrderDirection.BID, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) + + val takerSubmitOrderEvent = SubmitOrderEvent( + "touid", + "tuuid", + null, + pair, + 60000, + 1, + 0, + OrderDirection.ASK, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) + + val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) + //when + val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) + + Assertions.assertEquals(4, tradeFinancialActions.size) + Assertions.assertEquals( + makerSubmitOrderEvent.price * pairConfig.rightSideFraction, + tradeFinancialActions[1].amount.toDouble() + ) + } + } + + private fun makeTradeEvent( + pair: Pair, + takerSubmitOrderEvent: SubmitOrderEvent, + makerSubmitOrderEvent: SubmitOrderEvent + ): TradeEvent { + val tradeEvent = TradeEvent( + 0, + pair, + takerSubmitOrderEvent.ouid, + takerSubmitOrderEvent.uuid, + takerSubmitOrderEvent.orderId ?: -1, + takerSubmitOrderEvent.direction, + takerSubmitOrderEvent.price, + 0, + makerSubmitOrderEvent.ouid, + makerSubmitOrderEvent.uuid, + makerSubmitOrderEvent.orderId ?: 1, + makerSubmitOrderEvent.direction, + makerSubmitOrderEvent.price, + makerSubmitOrderEvent.quantity - takerSubmitOrderEvent.quantity, + takerSubmitOrderEvent.quantity + ) + return tradeEvent + } + + private fun prepareOrder( + pair: Pair, + pairConfig: PairConfig, + submitOrderEvent: SubmitOrderEvent, + makerFee: Double, + takerFee: Double + ) { + runBlocking { + Mockito.`when`(pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "")) + .thenReturn( + PairFeeConfig( + pairConfig, + submitOrderEvent.direction.toString(), + "", + makerFee, + takerFee + ) + ) + Mockito.`when`(financialActionPersister.persist(MockitoHelper.anyObject())) + .then { + return@then it.getArgument>(0) + } + + val financialActions = orderManager.handleRequestOrder(submitOrderEvent) + + val orderPairFeeConfig = + pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "") + val orderMakerFee = orderPairFeeConfig.makerFee * 1 //user level formula + val orderTakerFee = orderPairFeeConfig.takerFee * 1 //user level formula + Mockito.`when`(orderPersister.load(submitOrderEvent.ouid)).thenReturn( + Order( + submitOrderEvent.pair.toString(), + submitOrderEvent.ouid, + null, + orderMakerFee, + orderTakerFee, + orderPairFeeConfig.pairConfig.leftSideFraction, + orderPairFeeConfig.pairConfig.rightSideFraction, + submitOrderEvent.uuid, + "", + submitOrderEvent.direction, + submitOrderEvent.matchConstraint, + submitOrderEvent.orderType, + submitOrderEvent.price, + submitOrderEvent.quantity, + submitOrderEvent.quantity - submitOrderEvent.remainedQuantity, + submitOrderEvent.price.toBigDecimal(), + submitOrderEvent.quantity.toBigDecimal(), + (submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).toBigDecimal(), + financialActions[0].amount, + financialActions[0].amount, + 0 + ) + ) + } + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt index fe0ab8f0d..3123608f7 100644 --- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt +++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt @@ -1,30 +1,29 @@ -package co.nilin.opex.accountant.ports.kafka.listener.consumer - -import co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequest -import co.nilin.opex.accountant.ports.kafka.listener.spi.OrderSubmitRequestListener -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class OrderKafkaListener : MessageListener { - - val orderListeners = arrayListOf() - - override fun onMessage(data: ConsumerRecord) { - orderListeners.forEach { tl -> - tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) - } - - } - - fun addOrderListener(tl: OrderSubmitRequestListener) { - orderListeners.add(tl) - } - - fun removeOrderListener(tl: OrderSubmitRequestListener) { - orderListeners.removeIf { item -> - item.id() == tl.id() - } - } +package co.nilin.opex.accountant.ports.kafka.listener.consumer + +import co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequest +import co.nilin.opex.accountant.ports.kafka.listener.spi.OrderSubmitRequestListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.listener.MessageListener +import org.springframework.stereotype.Component + +@Component +class OrderKafkaListener : MessageListener { + + val orderListeners = arrayListOf() + + override fun onMessage(data: ConsumerRecord) { + orderListeners.forEach { tl -> + tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) + } + } + + fun addOrderListener(tl: OrderSubmitRequestListener) { + orderListeners.add(tl) + } + + fun removeOrderListener(tl: OrderSubmitRequestListener) { + orderListeners.removeIf { item -> + item.id() == tl.id() + } + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt index 724850ab4..849158e22 100644 --- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt +++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt @@ -5,37 +5,14 @@ import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.matching.engine.core.model.OrderType import co.nilin.opex.matching.engine.core.model.Pair -class OrderSubmitRequest() { - - lateinit var ouid: String - lateinit var uuid: String - var orderId: Long? = null - lateinit var pair: Pair - var price: Long = 0 - var quantity: Long = 0 - var direction: OrderDirection = OrderDirection.BID - var matchConstraint: MatchConstraint = MatchConstraint.GTC - var orderType: OrderType = OrderType.LIMIT_ORDER - - constructor( - ouid: String, - uuid: String, - orderId: Long?, - pair: Pair, - price: Long, - quantity: Long, - direction: OrderDirection, - matchConstraint: MatchConstraint, - orderType: OrderType - ) : this() { - this.ouid = ouid - this.uuid = uuid - this.orderId = orderId - this.pair = pair - this.price = price - this.quantity = quantity - this.direction = direction - this.matchConstraint = matchConstraint - this.orderType = orderType - } -} \ No newline at end of file +data class OrderSubmitRequest( + val ouid: String, + val uuid: String, + val orderId: Long?, + val pair: Pair, + val price: Long = 0, + val quantity: Long = 0, + val direction: OrderDirection, + val matchConstraint: MatchConstraint, + val orderType: OrderType, +) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/config/PostgresConfig.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/config/PostgresConfig.kt index 10fe331da..dd0fbf78e 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/config/PostgresConfig.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/config/PostgresConfig.kt @@ -11,17 +11,13 @@ import org.springframework.r2dbc.core.DatabaseClient @EnableR2dbcRepositories(basePackages = ["co.nilin.opex"]) class PostgresConfig( db: DatabaseClient, - @Value("classpath:schema.sql") private val schemaResource: Resource, - @Value("classpath:data.sql") private val dataResource: Resource? + @Value("classpath:schema.sql") private val schemaResource: Resource ) { init { val schemaReader = schemaResource.inputStream.reader() val schema = schemaReader.readText().trim() schemaReader.close() - val dataReader = dataResource?.inputStream?.reader() - val data = dataReader?.readText()?.trim() ?: "" - dataReader?.close() - val initDb = db.sql { schema.plus(data) } + val initDb = db.sql { schema } initDb // initialize the database .then() .subscribe() // execute diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt index 9f76a937b..4ebd1eee2 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt @@ -1,8 +1,20 @@ package co.nilin.opex.accountant.ports.postgres.dao import co.nilin.opex.accountant.ports.postgres.model.PairConfigModel +import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.reactive.ReactiveCrudRepository import org.springframework.stereotype.Repository +import reactor.core.publisher.Mono @Repository -interface PairConfigRepository : ReactiveCrudRepository +interface PairConfigRepository : ReactiveCrudRepository { + + @Query("insert into pair_config values (:pair, :leftSide, :rightSide, :leftSideFraction, :rightSideFraction) on conflict do nothing") + fun insert( + pair: String, + leftSide: String, + rightSide: String, + leftSideFraction: Double, + rightSideFraction: Double + ): Mono +} diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt index e5185f09a..015991e68 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt @@ -1,66 +1,67 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionStatus -import co.nilin.opex.accountant.core.spi.FinancialActionLoader -import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort -import org.springframework.stereotype.Component -import java.math.BigDecimal - -@Component -class FinancialActionLoaderImpl(val financialActionRepository: FinancialActionRepository) : FinancialActionLoader { - - override suspend fun loadUnprocessed(offset: Long, size: Long): List { - return financialActionRepository.findByStatus( - FinancialActionStatus.CREATED.name, - PageRequest.of(offset.toInt(), size.toInt(), Sort.by(Sort.Direction.ASC, "createDate")) - ).map { fim -> - loadFinancialAction(fim.id)!! - }.toList() - } - - override suspend fun findLast(uuid: String, ouid: String): FinancialAction? { - return financialActionRepository.findByOuidAndUuid( - ouid, uuid, PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createDate")) - ).map { fim -> - loadFinancialAction(fim.id) - }.firstOrNull() - } - - private suspend fun loadFinancialAction(id: Long?): FinancialAction? { - if (id != null) { - val fim = financialActionRepository.findById(id).awaitFirst() - return FinancialAction( - fim.id, - loadFinancialAction(fim.parentId), - fim.eventType, - fim.pointer, - fim.symbol, - BigDecimal.valueOf(fim.amount), - fim.sender, - fim.senderWalletType, - fim.receiver, - fim.receiverWalletType, - fim.createDate - ) - } - return null - } - - override suspend fun countUnprocessed(uuid: String, symbol: String, eventType: String): Long { - return financialActionRepository.findByUuidAndSymbolAndEventTypeAndStatus( - uuid, - symbol, - eventType, - FinancialActionStatus.CREATED - ).awaitFirstOrElse { BigDecimal.ZERO } - .toLong() - } +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionStatus +import co.nilin.opex.accountant.core.spi.FinancialActionLoader +import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Component +import java.math.BigDecimal + +@Component +class FinancialActionLoaderImpl(val financialActionRepository: FinancialActionRepository) : FinancialActionLoader { + + override suspend fun loadUnprocessed(offset: Long, size: Long): List { + return financialActionRepository.findByStatus( + FinancialActionStatus.CREATED.name, + PageRequest.of(offset.toInt(), size.toInt(), Sort.by(Sort.Direction.ASC, "createDate")) + ).map { fim -> + loadFinancialAction(fim.id)!! + }.toList() + } + + override suspend fun findLast(uuid: String, ouid: String): FinancialAction? { + return financialActionRepository.findByOuidAndUuid( + ouid, uuid, PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createDate")) + ).map { fim -> + loadFinancialAction(fim.id) + }.firstOrNull() + } + + private suspend fun loadFinancialAction(id: Long?): FinancialAction? { + if (id != null) { + val fim = financialActionRepository.findById(id).awaitFirst() + return FinancialAction( + loadFinancialAction(fim.parentId), + fim.eventType, + fim.pointer, + fim.symbol, + BigDecimal.valueOf(fim.amount), + fim.sender, + fim.senderWalletType, + fim.receiver, + fim.receiverWalletType, + fim.createDate, + fim.retryCount, + fim.id + ) + } + return null + } + + override suspend fun countUnprocessed(uuid: String, symbol: String, eventType: String): Long { + return financialActionRepository.findByUuidAndSymbolAndEventTypeAndStatus( + uuid, + symbol, + eventType, + FinancialActionStatus.CREATED + ).awaitFirstOrElse { BigDecimal.ZERO } + .toLong() + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt index f5102e7f5..b81bd76da 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt @@ -1,64 +1,64 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionStatus -import co.nilin.opex.accountant.core.spi.FinancialActionPersister -import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository -import co.nilin.opex.accountant.ports.postgres.model.FinancialActionModel -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitLast -import org.springframework.stereotype.Component -import java.time.LocalDateTime - -@Component -class FinancialActionPersisterImpl(val financialActionRepository: FinancialActionRepository) : - FinancialActionPersister { - - override suspend fun persist(financialActions: List): List { - financialActionRepository.saveAll(financialActions.map { fa -> - FinancialActionModel( - null, - fa.parent?.id, - fa.eventType, - fa.pointer, - fa.symbol, - fa.amount.toDouble(), - fa.sender, - fa.senderWalletType, - fa.receiver, - fa.receiverWalletType, - "", - "", - fa.createDate - ) - }).awaitLast() - return financialActions - } - - override suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) { - val existing = financialActionRepository.findById(financialAction.id!!).awaitFirstOrElse { - throw IllegalArgumentException() - } - financialActionRepository.save( - FinancialActionModel( - existing.id, - existing.parentId, - existing.eventType, - existing.pointer, - existing.symbol, - existing.amount, - existing.sender, - existing.senderWalletType, - existing.receiver, - existing.receiverWalletType, - existing.agent, - existing.ip, - existing.createDate, - status, - 1 + (existing.retryCount ?: 0), - LocalDateTime.now() - ) - ).awaitFirst() - } +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionStatus +import co.nilin.opex.accountant.core.spi.FinancialActionPersister +import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository +import co.nilin.opex.accountant.ports.postgres.model.FinancialActionModel +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitLast +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class FinancialActionPersisterImpl(val financialActionRepository: FinancialActionRepository) : + FinancialActionPersister { + + override suspend fun persist(financialActions: List): List { + financialActionRepository.saveAll(financialActions.map { + FinancialActionModel( + null, + it.parent?.id, + it.eventType, + it.pointer, + it.symbol, + it.amount.toDouble(), + it.sender, + it.senderWalletType, + it.receiver, + it.receiverWalletType, + "", + "", + it.createDate + ) + }).awaitLast() + return financialActions + } + + override suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) { + val existing = financialActionRepository.findById(financialAction.id!!).awaitFirstOrElse { + throw IllegalArgumentException() + } + financialActionRepository.save( + FinancialActionModel( + existing.id, + existing.parentId, + existing.eventType, + existing.pointer, + existing.symbol, + existing.amount, + existing.sender, + existing.senderWalletType, + existing.receiver, + existing.receiverWalletType, + existing.agent, + existing.ip, + existing.createDate, + status, + 1 + existing.retryCount, + LocalDateTime.now() + ) + ).awaitFirst() + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt index 947d8f898..d23810e2f 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt @@ -1,75 +1,87 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.accountant.core.spi.PairConfigLoader -import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository -import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository -import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.OpexException -import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Component - -@Component -class PairConfigLoaderImpl( - val pairConfigRepository: PairConfigRepository, val pairFeeConfigRepository: PairFeeConfigRepository -) : PairConfigLoader { - - override suspend fun loadPairConfigs(): List { - return pairConfigRepository.findAll() - .collectList() - .awaitFirstOrElse { emptyList() } - .map { - PairConfig( - it.pair, - it.leftSideWalletSymbol, - it.rightSideWalletSymbol, - it.leftSideFraction, - it.rightSideFraction - ) - } - } - - override suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { - val pairConfig = pairConfigRepository - .findById(pair).awaitFirstOrElse { - val error = OpexError.InvalidPair - throw OpexException(error, String.format(error.message!!, pair)) - } - var pairFeeConfig: PairFeeConfigModel? - if (userLevel.isEmpty()) { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, "*") - .awaitFirstOrElse { - val error = OpexError.InvalidPair - throw OpexException(error, String.format(error.message!!, pair)) - } - } else { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, userLevel) - .awaitFirstOrNull() - if (pairFeeConfig == null) { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, "*") - .awaitFirstOrElse { - val error = OpexError.InvalidPairFee - throw OpexException(error, String.format(error.message!!, pair)) - } - } - } - - return PairFeeConfig( - PairConfig( - pair, - pairConfig.leftSideWalletSymbol, - pairConfig.rightSideWalletSymbol, - pairConfig.leftSideFraction, - pairConfig.rightSideFraction - ), pairFeeConfig!!.direction, pairFeeConfig.userLevel, pairFeeConfig.makerFee, pairFeeConfig.takerFee - ) - - } +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.PairConfigLoader +import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository +import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository +import co.nilin.opex.accountant.ports.postgres.model.PairConfigModel +import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel +import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle +import org.springframework.stereotype.Component + +@Component +class PairConfigLoaderImpl( + val pairConfigRepository: PairConfigRepository, + val pairFeeConfigRepository: PairFeeConfigRepository +) : PairConfigLoader { + + override suspend fun loadPairConfigs(): List { + return pairConfigRepository.findAll() + .collectList() + .awaitFirstOrElse { emptyList() } + .map { it.asPairConfig() } + } + + override suspend fun loadPairFeeConfigs(): List { + return pairFeeConfigRepository.findAll() + .collectList() + .awaitFirstOrElse { emptyList() } + .map { + val pairConfig = pairConfigRepository.findById(it.pairConfigId).awaitSingle().asPairConfig() + PairFeeConfig(pairConfig, it.direction, it.userLevel, it.makerFee, it.takerFee) + } + } + + override suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { + val pairConfig = pairConfigRepository + .findById(pair).awaitFirstOrElse { + val error = OpexError.InvalidPair + throw OpexException(error, String.format(error.message!!, pair)) + } + var pairFeeConfig: PairFeeConfigModel? + if (userLevel.isEmpty()) { + pairFeeConfig = pairFeeConfigRepository + .findByPairAndDirectionAndUserLevel(pair, direction, "*") + .awaitFirstOrElse { + val error = OpexError.InvalidPair + throw OpexException(error, String.format(error.message!!, pair)) + } + } else { + pairFeeConfig = pairFeeConfigRepository + .findByPairAndDirectionAndUserLevel(pair, direction, userLevel) + .awaitFirstOrNull() + if (pairFeeConfig == null) { + pairFeeConfig = pairFeeConfigRepository + .findByPairAndDirectionAndUserLevel(pair, direction, "*") + .awaitFirstOrElse { + val error = OpexError.InvalidPairFee + throw OpexException(error, String.format(error.message!!, pair)) + } + } + } + + return PairFeeConfig( + PairConfig( + pair, + pairConfig.leftSideWalletSymbol, + pairConfig.rightSideWalletSymbol, + pairConfig.leftSideFraction, + pairConfig.rightSideFraction + ), pairFeeConfig!!.direction, pairFeeConfig.userLevel, pairFeeConfig.makerFee, pairFeeConfig.takerFee + ) + } + + private fun PairConfigModel.asPairConfig() = PairConfig( + pair, + leftSideWalletSymbol, + rightSideWalletSymbol, + leftSideFraction, + rightSideFraction + ) } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt deleted file mode 100644 index 1f0a9ae3d..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.spi.PairStaticRateLoader -import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository -import kotlinx.coroutines.reactive.awaitFirstOrElse -import org.springframework.stereotype.Component - -@Component -class PairStaticRateLoaderImpl(val pairConfigRepository: PairConfigRepository) : PairStaticRateLoader { - - override suspend fun calculateStaticRate(leftSide: String, rightSide: String): Double? { - val pairConfig = pairConfigRepository - .findById("${leftSide}_$rightSide") - .awaitFirstOrElse { throw IllegalArgumentException("${leftSide}_$rightSide is not available") } - return pairConfig.rate - } -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt index ef18db937..631e0c9b5 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt @@ -1,29 +1,29 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import co.nilin.opex.accountant.core.model.FinancialActionStatus -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table -import java.time.LocalDateTime - -@Table("fi_actions") -class FinancialActionModel( - @Id var id: Long?, - @Column("parent_id") var parentId: Long?, - @Column("event_type") val eventType: String, - val pointer: String, - val symbol: String, - @Column("amount") val amount: Double, - val sender: String, - @Column("sender_wallet_type") val senderWalletType: String, - val receiver: String, - @Column("receiver_wallet_type") val receiverWalletType: String, - val agent: String, - val ip: String, - @Column("create_date") val createDate: LocalDateTime, - val status: FinancialActionStatus = FinancialActionStatus.CREATED, - @Column("retry_count") val retryCount: Int? = null, - @Column("last_try_date") val lastTryDate: LocalDateTime? = null -) - - +package co.nilin.opex.accountant.ports.postgres.model + +import co.nilin.opex.accountant.core.model.FinancialActionStatus +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDateTime + +@Table("fi_actions") +class FinancialActionModel( + @Id var id: Long?, + @Column("parent_id") var parentId: Long?, + @Column("event_type") val eventType: String, + val pointer: String, + val symbol: String, + @Column("amount") val amount: Double, + val sender: String, + @Column("sender_wallet_type") val senderWalletType: String, + val receiver: String, + @Column("receiver_wallet_type") val receiverWalletType: String, + val agent: String, + val ip: String, + @Column("create_date") val createDate: LocalDateTime, + val status: FinancialActionStatus = FinancialActionStatus.CREATED, + @Column("retry_count") val retryCount: Int = 0, + @Column("last_try_date") val lastTryDate: LocalDateTime? = null +) + + diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt index 131f13268..fa07a137d 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt @@ -1,15 +1,14 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table - -@Table("pair_config") -data class PairConfigModel( - @Id val pair: String, - @Column("left_side_wallet_symbol") val leftSideWalletSymbol: String, //can be same as pair left side - @Column("right_side_wallet_symbol") val rightSideWalletSymbol: String, //can be same as pair right side - @Column("left_side_fraction") val leftSideFraction: Double, - @Column("right_side_fraction") val rightSideFraction: Double, - @Column("rate") val rate: Double +package co.nilin.opex.accountant.ports.postgres.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table + +@Table("pair_config") +data class PairConfigModel( + @Id val pair: String, + @Column("left_side_wallet_symbol") val leftSideWalletSymbol: String, //can be same as pair left side + @Column("right_side_wallet_symbol") val rightSideWalletSymbol: String, //can be same as pair right side + @Column("left_side_fraction") val leftSideFraction: Double, + @Column("right_side_fraction") val rightSideFraction: Double ) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/data.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/data.sql deleted file mode 100644 index d6cec7972..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/data.sql +++ /dev/null @@ -1,41 +0,0 @@ -INSERT INTO pair_config -VALUES ('btc_usdt', 'btc', 'usdt', 0.000001, 0.01, 55000), - ('eth_usdt', 'eth', 'usdt', 0.00001, 0.01, 3800), - ('nln_usdt', 'nln', 'usdt', 1.0, 0.01, 0.01), - ('nln_btc', 'nln', 'btc', 1.0, 0.000001, 1 / 5500000) -ON CONFLICT DO NOTHING; - --- Test pairs -INSERT INTO pair_config -VALUES ('tbtc_tusdt', 'tbtc', 'tusdt', 0.000001, 0.01, 55000), - ('teth_tusdt', 'teth', 'tusdt', 0.00001, 0.01, 3800), - ('nln_tusdt', 'nln', 'tusdt', 1.0, 0.01, 0.01), - ('nln_tbtc', 'nln', 'tbtc', 1.0, 0.000001, 1 / 5500000) -ON CONFLICT DO NOTHING; - -INSERT INTO pair_fee_config -VALUES (1, 'btc_usdt', 'ASK', '*', 0.01, 0.01), - (2, 'btc_usdt', 'BID', '*', 0.01, 0.01), - (3, 'nln_usdt', 'ASK', '*', 0.01, 0.01), - (4, 'nln_usdt', 'BID', '*', 0.01, 0.01), - (5, 'nln_btc', 'ASK', '*', 0.01, 0.01), - (6, 'nln_btc', 'BID', '*', 0.01, 0.01), - (7, 'eth_usdt', 'ASK', '*', 0.01, 0.01), - (8, 'eth_usdt', 'BID', '*', 0.01, 0.01) -ON CONFLICT DO NOTHING; - --- Test pair configs -INSERT INTO pair_fee_config -VALUES (9, 'tbtc_tusdt', 'ASK', '*', 0.01, 0.01), - (10, 'tbtc_tusdt', 'BID', '*', 0.01, 0.01), - (11, 'nln_tusdt', 'ASK', '*', 0.01, 0.01), - (12, 'nln_tusdt', 'BID', '*', 0.01, 0.01), - (13, 'nln_tbtc', 'ASK', '*', 0.01, 0.01), - (14, 'nln_tbtc', 'BID', '*', 0.01, 0.01), - (15, 'teth_tusdt', 'ASK', '*', 0.01, 0.01), - (16, 'teth_tusdt', 'BID', '*', 0.01, 0.01) -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('pair_fee_config', 'id'), (SELECT MAX(id) FROM pair_fee_config)); - -COMMIT; diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql index c87930e62..7db637242 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql @@ -1,84 +1,83 @@ -CREATE TABLE IF NOT EXISTS orders -( - id SERIAL PRIMARY KEY, - ouid VARCHAR(72) NOT NULL UNIQUE, - uuid VARCHAR(72) NOT NULL, - pair VARCHAR(72) NOT NULL, - matching_engine_id INTEGER, - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - left_side_fraction DECIMAL NOT NULL, - right_side_fraction DECIMAL NOT NULL, - user_level VARCHAR(20) NOT NULL, - direction VARCHAR(20) NOT NULL, - match_constraint VARCHAR(30) NOT NULL, - order_type VARCHAR(30) NOT NULL, - price DECIMAL NOT NULL, - quantity DECIMAL NOT NULL, - filled_quantity DECIMAL NOT NULL, - orig_price DECIMAL NOT NULL, - orig_quantity DECIMAL NOT NULL, - filled_orig_quantity DECIMAL NOT NULL, - first_transfer_amount DECIMAL NOT NULL, - remained_transfer_amount DECIMAL NOT NULL, - status INTEGER NOT NULL, - agent VARCHAR(20), - ip VARCHAR(11), - create_date TIMESTAMP NOT NULL -); - -CREATE TABLE IF NOT EXISTS fi_actions -( - id SERIAL PRIMARY KEY, - parent_id INTEGER, - event_type VARCHAR(72) NOT NULL, - pointer VARCHAR(72) NOT NULL, - symbol VARCHAR(36) NOT NULL, - amount DECIMAL NOT NULL, - sender VARCHAR(36) NOT NULL, - sender_wallet_type VARCHAR(36) NOT NULL, - receiver VARCHAR(36) NOT NULL, - receiver_wallet_type VARCHAR(36) NOT NULL, - agent VARCHAR(20), - ip VARCHAR(11), - create_date TIMESTAMP NOT NULL, - status VARCHAR(20), - retry_count DECIMAL, - last_try_date TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS pair_config -( - pair VARCHAR(72) PRIMARY KEY, - left_side_wallet_symbol VARCHAR(36) NOT NULL, - right_side_wallet_symbol VARCHAR(36) NOT NULL, - left_side_fraction DECIMAL NOT NULL, - right_side_fraction DECIMAL NOT NULL, - rate DECIMAL NOT NULL, - UNIQUE ( - left_side_wallet_symbol, - right_side_wallet_symbol - ) -); - -CREATE TABLE IF NOT EXISTS pair_fee_config -( - id SERIAL PRIMARY KEY, - pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), - direction VARCHAR(36) NOT NULL, - user_level VARCHAR(36) NOT NULL, - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - UNIQUE (direction, user_level, pair_config_id) -); - -CREATE TABLE IF NOT EXISTS temp_events -( - id SERIAL PRIMARY KEY, - ouid VARCHAR(72) NOT NULL, - event_type VARCHAR(72) NOT NULL, - event_body TEXT NOT NULL, - event_date TIMESTAMP NOT NULL -); - -COMMIT; +CREATE TABLE IF NOT EXISTS orders +( + id SERIAL PRIMARY KEY, + ouid VARCHAR(72) NOT NULL UNIQUE, + uuid VARCHAR(72) NOT NULL, + pair VARCHAR(72) NOT NULL, + matching_engine_id INTEGER, + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL, + left_side_fraction DECIMAL NOT NULL, + right_side_fraction DECIMAL NOT NULL, + user_level VARCHAR(20) NOT NULL, + direction VARCHAR(20) NOT NULL, + match_constraint VARCHAR(30) NOT NULL, + order_type VARCHAR(30) NOT NULL, + price DECIMAL NOT NULL, + quantity DECIMAL NOT NULL, + filled_quantity DECIMAL NOT NULL, + orig_price DECIMAL NOT NULL, + orig_quantity DECIMAL NOT NULL, + filled_orig_quantity DECIMAL NOT NULL, + first_transfer_amount DECIMAL NOT NULL, + remained_transfer_amount DECIMAL NOT NULL, + status INTEGER NOT NULL, + agent VARCHAR(20), + ip VARCHAR(11), + create_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS fi_actions +( + id SERIAL PRIMARY KEY, + parent_id INTEGER, + event_type VARCHAR(72) NOT NULL, + pointer VARCHAR(72) NOT NULL, + symbol VARCHAR(36) NOT NULL, + amount DECIMAL NOT NULL, + sender VARCHAR(36) NOT NULL, + sender_wallet_type VARCHAR(36) NOT NULL, + receiver VARCHAR(36) NOT NULL, + receiver_wallet_type VARCHAR(36) NOT NULL, + agent VARCHAR(20), + ip VARCHAR(11), + create_date TIMESTAMP NOT NULL, + status VARCHAR(20), + retry_count DECIMAL, + last_try_date TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS pair_config +( + pair VARCHAR(72) PRIMARY KEY, + left_side_wallet_symbol VARCHAR(36) NOT NULL, + right_side_wallet_symbol VARCHAR(36) NOT NULL, + left_side_fraction DECIMAL NOT NULL, + right_side_fraction DECIMAL NOT NULL, + UNIQUE ( + left_side_wallet_symbol, + right_side_wallet_symbol + ) +); + +CREATE TABLE IF NOT EXISTS pair_fee_config +( + id SERIAL PRIMARY KEY, + pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), + direction VARCHAR(36) NOT NULL, + user_level VARCHAR(36) NOT NULL, + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL, + UNIQUE (direction, user_level, pair_config_id) +); + +CREATE TABLE IF NOT EXISTS temp_events +( + id SERIAL PRIMARY KEY, + ouid VARCHAR(72) NOT NULL, + event_type VARCHAR(72) NOT NULL, + event_body TEXT NOT NULL, + event_date TIMESTAMP NOT NULL +); + +COMMIT; diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt index 2ddb2fe1e..b8ca2965f 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt @@ -2,6 +2,7 @@ package co.nilin.opex.accountant.ports.kafka.submitter.config import org.apache.kafka.clients.admin.NewTopic import org.apache.kafka.common.config.TopicConfig +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Configuration import org.springframework.context.support.GenericApplicationContext @@ -11,8 +12,12 @@ import java.util.function.Supplier @Configuration class KafkaTopicConfig { + private val logger = LoggerFactory.getLogger(KafkaTopicConfig::class.java) + @Autowired fun createTopics(applicationContext: GenericApplicationContext) { + logger.info("Creating kafka topic beans...") + with(applicationContext) { registerBean("topic_richOrder", NewTopic::class.java, Supplier { TopicBuilder.name("richOrder") @@ -32,6 +37,7 @@ class KafkaTopicConfig { registerBean("topic_tempevents", NewTopic::class.java, "tempevents", 1, 1) } + logger.info("Kafka topics created") } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt index 0de624e27..8eebc6280 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt @@ -1,63 +1,64 @@ -package co.nilin.opex.accountant.ports.kafka.submitter.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.producer.ProducerConfig -import org.apache.kafka.common.serialization.StringSerializer -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.DefaultKafkaProducerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.core.ProducerFactory -import org.springframework.kafka.support.serializer.JsonSerializer - -@Configuration -class SubmitterKafkaConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Bean("producerConfigs") - fun producerConfigs(): Map { - return mapOf( - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, - ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, - ProducerConfig.ACKS_CONFIG to "all", - //ProducerConfig.CLIENT_ID_CONFIG to "", omitting this option as it produces InstanceAlreadyExistsException - ) - } - - @Bean("accountantEventProducerFactory") - fun producerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("accountantEventKafkaTemplate") - fun kafkaTemplate(@Qualifier("accountantEventProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } - - @Bean("richTradeProducerFactory") - fun richTradeProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richTradeKafkaTemplate") - fun richTradeKafkaTemplate(@Qualifier("richTradeProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } - - @Bean("richOrderProducerFactory") - fun richOrderProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richOrderKafkaTemplate") - fun richOrderKafkaTemplate(@Qualifier("richOrderProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } +package co.nilin.opex.accountant.ports.kafka.submitter.config + +import co.nilin.opex.accountant.core.inout.RichOrderEvent +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.support.serializer.JsonSerializer + +@Configuration +class SubmitterKafkaConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Bean("producerConfigs") + fun producerConfigs(): Map { + return mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, + ProducerConfig.ACKS_CONFIG to "all", + JsonSerializer.TYPE_MAPPINGS to "rich_order_event:co.nilin.opex.accountant.core.inout.RichOrderEvent,rich_order:co.nilin.opex.accountant.core.inout.RichOrder,rich_order_update:co.nilin.opex.accountant.core.inout.RichOrderUpdate, rich_trade:co.nilin.opex.accountant.core.inout.RichTrade" + //ProducerConfig.CLIENT_ID_CONFIG to "", omitting this option as it produces InstanceAlreadyExistsException + ) + } + + @Bean("accountantEventProducerFactory") + fun producerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("accountantEventKafkaTemplate") + fun kafkaTemplate(@Qualifier("accountantEventProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } + + @Bean("richTradeProducerFactory") + fun richTradeProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richTradeKafkaTemplate") + fun richTradeKafkaTemplate(@Qualifier("richTradeProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } + + @Bean("richOrderProducerFactory") + fun richOrderProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richOrderKafkaTemplate") + fun richOrderKafkaTemplate(@Qualifier("richOrderProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt index 05c2e7189..0a93b1b2c 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt @@ -23,8 +23,8 @@ class RichOrderSubmitter(@Qualifier("richOrderKafkaTemplate") val kafkaTemplate: sendFuture.addCallback({ cont.resume(Unit) }, { - logger.info("Error submitting RichOrder", it) - cont.resumeWithException(it) + logger.error("Error submitting RichOrder", it) + cont.resume(Unit) }) } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt index e35188ada..c5f3641af 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt @@ -24,7 +24,7 @@ class RichTradeSubmitter(@Qualifier("richTradeKafkaTemplate") val kafkaTemplate: cont.resume(Unit) }, { logger.error("RichTrade submitter error", it) - cont.resumeWithException(it) + cont.resume(Unit) }) } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml index e67d5ee82..bf441f472 100644 --- a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml +++ b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml @@ -40,10 +40,6 @@ org.springframework.cloud spring-cloud-starter-consul-all - - org.springframework.boot - spring-boot-starter-actuator - org.springframework.boot spring-boot-starter-data-r2dbc diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt new file mode 100644 index 000000000..1a38bcadf --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +import java.math.BigDecimal + +data class Amount(val currency: Currency, val amount: BigDecimal) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt new file mode 100644 index 000000000..2061a0b91 --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +data class BooleanResponse(val result: Boolean) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt new file mode 100644 index 000000000..af8fccc29 --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +data class Currency(val name: String, val symbol: String, val precision: Int) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt new file mode 100644 index 000000000..ed426824d --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +data class TransferResult( + val date: Long, + val sourceUuid: String, + val sourceWalletType: String, + val sourceBalanceBeforeAction: Amount, + val sourceBalanceAfterAction: Amount, + val amount: Amount, + val destUuid: String, + val destWalletType: String, + val receivedAmount: Amount +) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt index 69616db32..5acb55996 100644 --- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt @@ -1,71 +1,50 @@ -package co.nilin.opex.accountant.ports.walletproxy.proxy - -import co.nilin.opex.accountant.core.spi.WalletProxy -import kotlinx.coroutines.reactive.awaitFirst -import org.springframework.beans.factory.annotation.Value -import org.springframework.core.ParameterizedTypeReference -import org.springframework.stereotype.Component -import org.springframework.web.reactive.function.client.WebClient -import java.math.BigDecimal -import java.net.URI - -inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {} -data class TransferResult( - val date: Long, - val sourceUuid: String, - val sourceWalletType: String, - val sourceBalanceBeforeAction: Amount, - val sourceBalanceAfterAction: Amount, - val amount: Amount, - val destUuid: String, - val destWalletType: String, - val receivedAmount: Amount -) - -data class Amount(val currency: Currency, val amount: BigDecimal) -data class Currency(val name: String, val symbol: String, val precision: Int) - -@Component -class WalletProxyImpl( - @Value("\${app.wallet.url}") val walletBaseUrl: String, val webClient: WebClient -) : WalletProxy { - override suspend fun transfer( - symbol: String, - senderWalletType: String, - senderUuid: String, - receiverWalletType: String, - receiverUuid: String, - amount: BigDecimal, - description: String?, - transferRef: String? - ) { - webClient.post() - .uri(URI.create("$walletBaseUrl/transfer/${amount}_${symbol}/from/${senderUuid}_${senderWalletType}/to/${receiverUuid}_${receiverWalletType}")) - .header("Content-Type", "application/json") - .retrieve() - .onStatus({ t -> t.isError }, { p -> - /* - p.bodyToMono(typeRef>()).map { t -> KycSejamException(p.statusCode().value().toString(), t.error?.errorCode.toString() - + "-" + t.error?.customMessage) } - */ - throw RuntimeException() - }) - .bodyToMono(typeRef()) - .log() - .awaitFirst() - - } - - override suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean { - data class BooleanResponse(val result: Boolean) - return webClient.get() - .uri(URI.create("$walletBaseUrl/$uuid/wallet_type/${walletType}/can_withdraw/${amount}_${symbol}")) - .header("Content-Type", "application/json") - .retrieve() - .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) - .log() - .awaitFirst() - .result - } +package co.nilin.opex.accountant.ports.walletproxy.proxy + +import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse +import co.nilin.opex.accountant.ports.walletproxy.data.TransferResult +import kotlinx.coroutines.reactive.awaitFirst +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono +import java.math.BigDecimal + +@Component +class WalletProxyImpl( + @Value("\${app.wallet.url}") val walletBaseUrl: String, + val webClient: WebClient +) : WalletProxy { + + override suspend fun transfer( + symbol: String, + senderWalletType: String, + senderUuid: String, + receiverWalletType: String, + receiverUuid: String, + amount: BigDecimal, + description: String?, + transferRef: String? + ) { + webClient.post() + .uri("$walletBaseUrl/transfer/${amount}_$symbol/from/${senderUuid}_$senderWalletType/to/${receiverUuid}_$receiverWalletType?transferRef=$transferRef&description=$description") + .header("Content-Type", "application/json") + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono() + .log() + .awaitFirst() + } + + override suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean { + return webClient.get() + .uri("$walletBaseUrl/$uuid/wallet_type/$walletType/can_withdraw/${amount}_$symbol") + .header("Content-Type", "application/json") + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono() + .log() + .awaitFirst() + .result + } } \ No newline at end of file diff --git a/accountant/pom.xml b/accountant/pom.xml index c60d9c6c1..01387a319 100644 --- a/accountant/pom.xml +++ b/accountant/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -73,6 +73,11 @@ logging-handler ${project.version} + + co.nilin.opex.utility.preferences + preferences + ${project.version} + org.springframework.cloud spring-cloud-dependencies diff --git a/admin/admin-app/Dockerfile b/admin/admin-app/Dockerfile index 6916b628a..d082a3221 100644 --- a/admin/admin-app/Dockerfile +++ b/admin/admin-app/Dockerfile @@ -2,4 +2,5 @@ FROM openjdk:11 VOLUME /tmp ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/controller/AuthAdminController.kt b/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/controller/AuthAdminController.kt index 0631ab24e..31ba94a53 100644 --- a/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/controller/AuthAdminController.kt +++ b/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/controller/AuthAdminController.kt @@ -1,63 +1,68 @@ -package co.nilin.opex.admin.ports.auth.controller - -import co.nilin.opex.admin.ports.auth.data.* -import co.nilin.opex.admin.ports.auth.service.AuthAdminService -import co.nilin.opex.admin.ports.auth.utils.asKeycloakUser -import org.springframework.http.MediaType -import org.springframework.web.bind.annotation.* - -@RestController -@RequestMapping("/auth/v1") -class AuthAdminController(private val service: AuthAdminService) { - - @GetMapping("/user") - suspend fun getAllKeycloakUsers(@RequestParam offset: Int, @RequestParam size: Int): QueryUserResponse { - return service.findAllUsers(offset, size) - } - - @GetMapping("/user/{userId}") - suspend fun getUser(@PathVariable userId: String): KeycloakUser { - return service.getUser(userId).asKeycloakUser(true).apply { - groups = service.getUserGroups(userId).map { KeycloakGroup(it.id, it.name) } - } - } - - @PostMapping("/user/{userId}/join-kyc") - fun switchKYCGroup(@PathVariable userId: String, @RequestParam kycGroup: KycGroup) { - service.switchKYCGroup(userId, kycGroup) - } - - @PostMapping("/user/{userId}/kyc/accept") - fun acceptKYC(@PathVariable userId: String) { - service.switchKYCGroup(userId, KycGroup.ACCEPTED) - } - - @PostMapping("/user/impersonate", produces = [MediaType.APPLICATION_JSON_VALUE]) - suspend fun impersonate(@RequestBody body: ImpersonateRequest): String { - return service.impersonate(body.clientId, body.clientSecret, body.userId) - } - - @GetMapping("/user/search") - suspend fun searchUsers( - @RequestParam search: String, - @RequestParam(required = false) by: String?, - @RequestParam offset: Int, - @RequestParam size: Int - ): QueryUserResponse { - return if (by == "email") service.searchUserEmail(search) - else service.searchUser(search, offset, size) - } - - @PostMapping("/user/{userId}/kyc/reject") - fun rejectKYC(@PathVariable userId: String) { - service.switchKYCGroup(userId, KycGroup.REJECTED) - } - - @GetMapping("/group/{groupName}/members") - fun getMembersOfGroup( - @PathVariable groupName: String, @RequestParam offset: Int, @RequestParam size: Int - ): QueryUserResponse { - return service.findUsersInGroupByName(groupName, offset, size) - } - +package co.nilin.opex.admin.ports.auth.controller + +import co.nilin.opex.admin.ports.auth.data.* +import co.nilin.opex.admin.ports.auth.service.AuthAdminService +import co.nilin.opex.admin.ports.auth.utils.asKeycloakUser +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/auth/v1") +class AuthAdminController(private val service: AuthAdminService) { + + @GetMapping("/user") + suspend fun getAllKeycloakUsers(@RequestParam offset: Int, @RequestParam size: Int): QueryUserResponse { + return service.findAllUsers(offset, size) + } + + @GetMapping("/user/{userId}") + suspend fun getUser(@PathVariable userId: String): KeycloakUser { + return service.getUser(userId).asKeycloakUser(true).apply { + groups = service.getUserGroups(userId).map { KeycloakGroup(it.id, it.name) } + } + } + + @PostMapping("/user/{userId}/join-kyc") + fun switchKYCGroup(@PathVariable userId: String, @RequestParam kycGroup: KycGroup) { + service.switchKYCGroup(userId, kycGroup) + } + + @PostMapping("/user/{userId}/kyc/accept") + fun acceptKYC(@PathVariable userId: String) { + service.switchKYCGroup(userId, KycGroup.ACCEPTED) + } + + @PostMapping("/user/{userId}/kyc/reject") + fun rejectKYC(@PathVariable userId: String, @RequestParam reason: String) { + service.rejectKYC(userId, reason) + } + + @PostMapping("/user/{userId}/kyc/block") + fun blockKYC(@PathVariable userId: String, @RequestParam reason: String) { + service.blockKYC(userId, reason) + } + + @PostMapping("/user/impersonate", produces = [MediaType.APPLICATION_JSON_VALUE]) + suspend fun impersonate(@RequestBody body: ImpersonateRequest): String { + return service.impersonate(body.clientId, body.clientSecret, body.userId) + } + + @GetMapping("/user/search") + suspend fun searchUsers( + @RequestParam search: String, + @RequestParam(required = false) by: String?, + @RequestParam offset: Int, + @RequestParam size: Int + ): QueryUserResponse { + return if (by == "email") service.searchUserEmail(search) + else service.searchUser(search, offset, size) + } + + @GetMapping("/group/{groupName}/members") + fun getMembersOfGroup( + @PathVariable groupName: String, @RequestParam offset: Int, @RequestParam size: Int + ): QueryUserResponse { + return service.findUsersInGroupByName(groupName, offset, size) + } + } \ No newline at end of file diff --git a/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/data/KycGroup.kt b/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/data/KycGroup.kt index b6c1d27ba..9d542d805 100644 --- a/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/data/KycGroup.kt +++ b/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/data/KycGroup.kt @@ -1,9 +1,10 @@ -package co.nilin.opex.admin.ports.auth.data - -enum class KycGroup(val groupName: String) { - - REQUESTED("kyc-requested"), - ACCEPTED("kyc-accepted"), - REJECTED("kyc-rejected") - +package co.nilin.opex.admin.ports.auth.data + +enum class KycGroup(val groupName: String) { + + REQUESTED("kyc-requested"), + ACCEPTED("kyc-accepted"), + REJECTED("kyc-rejected"), + BLOCKED("kyc-blocked") + } \ No newline at end of file diff --git a/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/service/AuthAdminService.kt b/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/service/AuthAdminService.kt index 92047b189..7160978e1 100644 --- a/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/service/AuthAdminService.kt +++ b/admin/admin-ports/admin-service-auth/src/main/kotlin/co/nilin/opex/admin/ports/auth/service/AuthAdminService.kt @@ -1,105 +1,123 @@ -package co.nilin.opex.admin.ports.auth.service - -import co.nilin.opex.admin.ports.auth.data.KycGroup -import co.nilin.opex.admin.ports.auth.data.QueryUserResponse -import co.nilin.opex.admin.ports.auth.proxy.KeycloakProxy -import co.nilin.opex.admin.ports.auth.utils.asKeycloakUser -import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.OpexException -import org.keycloak.admin.client.Keycloak -import org.keycloak.admin.client.resource.GroupResource -import org.keycloak.admin.client.resource.RealmResource -import org.keycloak.representations.idm.GroupRepresentation -import org.keycloak.representations.idm.UserRepresentation -import org.springframework.stereotype.Service - -@Service -class AuthAdminService( - private val keycloak: Keycloak, - private val opexRealm: RealmResource, - private val proxy: KeycloakProxy -) { - - fun getUser(userId: String): UserRepresentation { - return opexRealm.users().get(userId).toRepresentation() ?: throw OpexException(OpexError.UserNotFoundAdmin) - } - - fun getUserGroups(userId: String): List { - return opexRealm.users().get(userId).groups() - } - - fun findAllUsers(offset: Int, size: Int): QueryUserResponse { - return QueryUserResponse( - opexRealm.users().count(), - opexRealm.users().list(offset, size).map { it.asKeycloakUser() } - ) - } - - fun findGroupById(groupId: String): GroupResource { - return opexRealm.groups().group(groupId) ?: throw OpexException(OpexError.NotFound, "Group not found") - } - - fun findGroupByName(groupName: String): GroupResource { - val groupRep = opexRealm.groups() - .groups() - .find { it.name == groupName } - ?: throw OpexException(OpexError.NotFound, "Group not found") - - return opexRealm.groups().group(groupRep.id) - } - - fun findUsersInGroupById(groupId: String): List { - val group = findGroupById(groupId) - return group.members() - } - - fun findUsersInGroupByName(groupName: String, offset: Int, size: Int): QueryUserResponse { - val group = findGroupByName(groupName) - val members = group.members(offset, size) - return QueryUserResponse( - members.count(), - members.map { it.asKeycloakUser() } - ) - } - - fun addUserToGroup(userId: String, groupId: String) { - val user = opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") - user.joinGroup(groupId) - } - - fun removeUserFromGroup(userId: String, groupId: String) { - val user = opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") - user.leaveGroup(groupId) - } - - fun switchKYCGroup(userId: String, kycGroup: KycGroup) { - val group = findGroupByName(kycGroup.groupName) - val user = opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") - with(user) { - groups().forEach { leaveGroup(it.id) } - joinGroup(group.toRepresentation().id) - } - } - - suspend fun impersonate(clientId: String, clientSecret: String, userId: String): String { - opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") - val token = keycloak.tokenManager().accessToken.token - return proxy.impersonate(token, clientId, clientSecret, userId) - } - - fun searchUser(search: String, offset: Int, size: Int): QueryUserResponse { - return QueryUserResponse( - opexRealm.users().search(search).count(), - opexRealm.users().search(search, offset, size, false).map { it.asKeycloakUser() } - ) - } - - fun searchUserEmail(search: String): QueryUserResponse { - val users = opexRealm.users().search(search) - return QueryUserResponse( - users.count(), - users.map { it.asKeycloakUser() } - ) - } - +package co.nilin.opex.admin.ports.auth.service + +import co.nilin.opex.admin.ports.auth.data.KycGroup +import co.nilin.opex.admin.ports.auth.data.QueryUserResponse +import co.nilin.opex.admin.ports.auth.proxy.KeycloakProxy +import co.nilin.opex.admin.ports.auth.utils.asKeycloakUser +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import org.keycloak.admin.client.Keycloak +import org.keycloak.admin.client.resource.GroupResource +import org.keycloak.admin.client.resource.RealmResource +import org.keycloak.representations.idm.GroupRepresentation +import org.keycloak.representations.idm.UserRepresentation +import org.springframework.stereotype.Service + +@Service +class AuthAdminService( + private val keycloak: Keycloak, + private val opexRealm: RealmResource, + private val proxy: KeycloakProxy +) { + + fun getUser(userId: String): UserRepresentation { + return opexRealm.users().get(userId).toRepresentation() ?: throw OpexException(OpexError.UserNotFoundAdmin) + } + + fun getUserGroups(userId: String): List { + return opexRealm.users().get(userId).groups() + } + + fun findAllUsers(offset: Int, size: Int): QueryUserResponse { + return QueryUserResponse( + opexRealm.users().count(), + opexRealm.users().list(offset, size).map { it.asKeycloakUser() } + ) + } + + fun findGroupById(groupId: String): GroupResource { + return opexRealm.groups().group(groupId) ?: throw OpexException(OpexError.NotFound, "Group not found") + } + + fun findGroupByName(groupName: String): GroupResource { + val groupRep = opexRealm.groups() + .groups() + .find { it.name == groupName } + ?: throw OpexException(OpexError.NotFound, "Group not found") + + return opexRealm.groups().group(groupRep.id) + } + + fun findUsersInGroupById(groupId: String): List { + val group = findGroupById(groupId) + return group.members() + } + + fun findUsersInGroupByName(groupName: String, offset: Int, size: Int): QueryUserResponse { + val group = findGroupByName(groupName) + val members = group.members(offset, size) + return QueryUserResponse( + members.count(), + members.map { it.asKeycloakUser() } + ) + } + + fun addUserToGroup(userId: String, groupId: String) { + val user = opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") + user.joinGroup(groupId) + } + + fun removeUserFromGroup(userId: String, groupId: String) { + val user = opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") + user.leaveGroup(groupId) + } + + fun rejectKYC(userId: String, reason: String) { + switchKYCGroup(userId, KycGroup.REJECTED) + val user = opexRealm.users().get(userId) + with(user.toRepresentation()) { + attributes[".rejectReason"] = mutableListOf(reason) + user.update(this) + } + } + + fun blockKYC(userId: String, reason: String) { + switchKYCGroup(userId, KycGroup.BLOCKED) + val user = opexRealm.users().get(userId) + with(user.toRepresentation()) { + attributes[".blockReason"] = mutableListOf(reason) + user.update(this) + } + } + + fun switchKYCGroup(userId: String, kycGroup: KycGroup) { + val group = findGroupByName(kycGroup.groupName) + val user = opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") + with(user) { + groups().forEach { leaveGroup(it.id) } + joinGroup(group.toRepresentation().id) + } + } + + suspend fun impersonate(clientId: String, clientSecret: String, userId: String): String { + opexRealm.users().get(userId) ?: throw OpexException(OpexError.NotFound, "User not found") + val token = keycloak.tokenManager().accessToken.token + return proxy.impersonate(token, clientId, clientSecret, userId) + } + + fun searchUser(search: String, offset: Int, size: Int): QueryUserResponse { + return QueryUserResponse( + opexRealm.users().search(search).count(), + opexRealm.users().search(search, offset, size, false).map { it.asKeycloakUser() } + ) + } + + fun searchUserEmail(search: String): QueryUserResponse { + val users = opexRealm.users().search(search) + return QueryUserResponse( + users.count(), + users.map { it.asKeycloakUser() } + ) + } + } \ No newline at end of file diff --git a/admin/pom.xml b/admin/pom.xml index 3a6c9ae9e..a82c81d20 100644 --- a/admin/pom.xml +++ b/admin/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/api/api-app/Dockerfile b/api/api-app/Dockerfile index 7c71f9447..268392261 100644 --- a/api/api-app/Dockerfile +++ b/api/api-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml index 00529ca01..4d69c7fc1 100644 --- a/api/api-app/pom.xml +++ b/api/api-app/pom.xml @@ -39,10 +39,6 @@ co.nilin.opex.utility.interceptors interceptors - - co.nilin.opex.accountant.core - accountant-core - co.nilin.opex.api.core api-core @@ -68,6 +64,10 @@ org.springframework.cloud spring-cloud-starter-vault-config + + co.nilin.opex.utility.preferences + preferences + diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt new file mode 100644 index 000000000..724f59cb4 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.api.app.config + +import co.nilin.opex.api.ports.postgres.dao.SymbolMapRepository +import co.nilin.opex.api.ports.postgres.model.SymbolMapModel +import co.nilin.opex.utility.preferences.Preferences +import kotlinx.coroutines.reactor.awaitSingleOrNull +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.DependsOn +import org.springframework.stereotype.Component +import javax.annotation.PostConstruct + +@Component +@DependsOn("postgresConfig") +class InitializeService(private val symbolMapRepository: SymbolMapRepository) { + @Autowired + private lateinit var preferences: Preferences + + @PostConstruct + fun init() = runBlocking { + preferences.markets.map { + val pair = it.pair ?: "${it.leftSide}_${it.rightSide}" + val items = it.aliases.map { a -> SymbolMapModel(null, pair, a.key, a.alias) } + runCatching { symbolMapRepository.saveAll(items).collectList().awaitSingleOrNull() } + } + } +} diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt index 231209348..8b8d1fda4 100644 --- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt @@ -1,38 +1,38 @@ -package co.nilin.opex.api.app.listener - -import co.nilin.opex.accountant.core.inout.RichOrder -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichOrderUpdate -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.app.config.AppDispatchers -import co.nilin.opex.api.core.spi.OrderPersister -import co.nilin.opex.api.core.spi.TradePersister -import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener -import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener -import kotlinx.coroutines.runBlocking - -class ApiListenerImpl( - private val richOrderPersister: OrderPersister, - private val richTradePersister: TradePersister -) : RichTradeListener, RichOrderListener { - - override fun id(): String { - return "AppListener" - } - - override fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) { - println("RichTrade received") - runBlocking(AppDispatchers.kafkaExecutor) { - richTradePersister.save(trade) - } - } - - override fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) { - runBlocking(AppDispatchers.kafkaExecutor) { - when (order) { - is RichOrder -> richOrderPersister.save(order) - is RichOrderUpdate -> richOrderPersister.update(order) - } - } - } +package co.nilin.opex.api.app.listener + +import co.nilin.opex.api.app.config.AppDispatchers +import co.nilin.opex.api.core.event.RichOrder +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.core.event.RichOrderUpdate +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.core.spi.OrderPersister +import co.nilin.opex.api.core.spi.TradePersister +import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener +import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener +import kotlinx.coroutines.runBlocking + +class ApiListenerImpl( + private val richOrderPersister: OrderPersister, + private val richTradePersister: TradePersister +) : RichTradeListener, RichOrderListener { + + override fun id(): String { + return "AppListener" + } + + override fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) { + println("RichTrade received") + runBlocking(AppDispatchers.kafkaExecutor) { + richTradePersister.save(trade) + } + } + + override fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) { + runBlocking(AppDispatchers.kafkaExecutor) { + when (order) { + is RichOrder -> richOrderPersister.save(order) + is RichOrderUpdate -> richOrderPersister.update(order) + } + } + } } \ No newline at end of file diff --git a/api/api-core/pom.xml b/api/api-core/pom.xml index 0a8539071..07b42dac9 100644 --- a/api/api-core/pom.xml +++ b/api/api-core/pom.xml @@ -1,52 +1,44 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - - - co.nilin.opex.api.core - api-core - api-core - Api logic of Opex - - - - org.jetbrains.kotlin - kotlin-reflect - - - org.springframework.boot - spring-boot-starter - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.accountant.core - accountant-core - - - org.springframework - spring-tx - provided - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + + + co.nilin.opex.api.core + api-core + api-core + Api logic of Opex + + + + org.jetbrains.kotlin + kotlin-reflect + + + org.springframework.boot + spring-boot-starter + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + org.springframework + spring-tx + provided + + + diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt new file mode 100644 index 000000000..2bccaf32e --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.api.core.event + +import co.nilin.opex.api.core.inout.MatchConstraint +import co.nilin.opex.api.core.inout.MatchingOrderType +import co.nilin.opex.api.core.inout.OrderDirection +import java.math.BigDecimal + +data class RichOrder( + val orderId: Long? = 0, + val pair: String, + val ouid: String, + val uuid: String, + val userLevel: String, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + val leftSideFraction: BigDecimal, + val rightSideFraction: BigDecimal, + val direction: OrderDirection, + val constraint: MatchConstraint, + val type: MatchingOrderType, + val price: BigDecimal, + val quantity: BigDecimal, + val quoteQuantity: BigDecimal, + val executedQuantity: BigDecimal, + val accumulativeQuoteQty: BigDecimal, + val status: Int = 0 +) : RichOrderEvent diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt new file mode 100644 index 000000000..a953bf91d --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.api.core.event + +interface RichOrderEvent \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt new file mode 100644 index 000000000..2dbff5ab1 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.api.core.event + +import co.nilin.opex.api.core.inout.OrderStatus +import java.math.BigDecimal + +data class RichOrderUpdate( + val ouid: String, + val price: BigDecimal, + val quantity: BigDecimal, + val remainedQuantity: BigDecimal, + val status: OrderStatus = OrderStatus.NEW +) : RichOrderEvent { + + fun executedQuantity(): BigDecimal = quantity.minus(remainedQuantity) + + fun accumulativeQuoteQuantity(): BigDecimal = price.multiply((quantity.minus(remainedQuantity))) + +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt new file mode 100644 index 000000000..8253191bf --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt @@ -0,0 +1,32 @@ +package co.nilin.opex.api.core.event + +import co.nilin.opex.api.core.inout.OrderDirection +import java.math.BigDecimal +import java.time.LocalDateTime + +class RichTrade( + val id: Long, + val pair: String, + val takerOuid: String, + val takerUuid: String, + val takerOrderId: Long, + val takerDirection: OrderDirection, + val takerPrice: BigDecimal, + val takerQuantity: BigDecimal, + val takerQuoteQuantity: BigDecimal, + val takerRemainedQuantity: BigDecimal, + val takerCommision: BigDecimal, + val takerCommisionAsset: String, + val makerOuid: String, + val makerUuid: String, + val makerOrderId: Long, + val makerDirection: OrderDirection, + val makerPrice: BigDecimal, + val makerQuantity: BigDecimal, + val makerQuoteQuantity: BigDecimal, + val makerRemainedQuantity: BigDecimal, + val makerCommision: BigDecimal, + val makerCommisionAsset: String, + val matchedQuantity: BigDecimal, + val tradeDateTime: LocalDateTime +) \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt index 70c43fec1..fe558f43e 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt @@ -1,41 +1,64 @@ -package co.nilin.opex.api.core.inout - -enum class TimeInForce { - GTC, //Good Til Canceled, An order will be on the book unless the order is canceled. - IOC, //Immediate Or Cancel, An order will try to fill the order as much as it can before the order expires. - FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution. -} - -enum class OrderStatus(val code: Int) { - - NEW(1), //The order has been accepted by the engine. - PARTIALLY_FILLED(4), //A part of the order has been filled. - FILLED(5), //The order has been completed. - CANCELED(2), //The order has been canceled by the user. - PENDING_CANCEL(7), //Currently unused - REJECTED(3), //The order was not accepted by the engine and not processed. - EXPIRED(6) //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) -} - -enum class OrderType { - LIMIT, // timeInForce, quantity, price - MARKET, // quantity or quoteOrderQty - STOP_LOSS, // quantity, stopPrice - STOP_LOSS_LIMIT, // timeInForce, quantity, price, stopPrice - TAKE_PROFIT, // quantity, stopPrice - TAKE_PROFIT_LIMIT, // timeInForce, quantity, price, stopPrice - LIMIT_MAKER; // quantity, price - - companion object { - fun activeTypes() = listOf(LIMIT, MARKET) - } -} - -enum class OrderSide { - BUY, - SELL -} - -enum class OrderResponseType { - ACK, RESULT, FULL +package co.nilin.opex.api.core.inout + +enum class TimeInForce { + GTC, //Good Til Canceled, An order will be on the book unless the order is canceled. + IOC, //Immediate Or Cancel, An order will try to fill the order as much as it can before the order expires. + FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution. +} + +enum class OrderStatus(val code: Int, val orderOfAppearance: Int) { + + REQUESTED(0, 0), + NEW(1, 1), //The order has been accepted by the engine. + PARTIALLY_FILLED(4, 2), //A part of the order has been filled. + FILLED(5, 3), //The order has been completed. + CANCELED(2, 3), //The order has been canceled by the user. + REJECTED(3, 3), //The order was not accepted by the engine and not processed. + EXPIRED( + 6, + 3 + ); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) + + fun comesBefore(status: OrderStatus?): Boolean { + if (status == null) + return false + return orderOfAppearance < status.orderOfAppearance + } + + fun comesAfter(status: OrderStatus?): Boolean { + if (status == null) + return false + return orderOfAppearance > status.orderOfAppearance + } + + companion object { + fun fromCode(code: Int?): OrderStatus? { + if (code == null) + return null + return values().find { it.code == code } + } + } +} + +enum class OrderType { + LIMIT, // timeInForce, quantity, price + MARKET, // quantity or quoteOrderQty + STOP_LOSS, // quantity, stopPrice + STOP_LOSS_LIMIT, // timeInForce, quantity, price, stopPrice + TAKE_PROFIT, // quantity, stopPrice + TAKE_PROFIT_LIMIT, // timeInForce, quantity, price, stopPrice + LIMIT_MAKER; // quantity, price + + companion object { + fun activeTypes() = listOf(LIMIT, MARKET) + } +} + +enum class OrderSide { + BUY, + SELL +} + +enum class OrderResponseType { + ACK, RESULT, FULL } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt new file mode 100644 index 000000000..e46e16de8 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.api.core.inout + +enum class OrderDirection { + ASK, BID +} + +enum class MatchConstraint { + GTC, + IOC, + IOC_BUDGET, + FOK, + FOK_BUDGET +} + +enum class MatchingOrderType { + LIMIT_ORDER, MARKET_ORDER +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairFeeResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairFeeResponse.kt new file mode 100644 index 000000000..2b3797a11 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairFeeResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.api.core.inout + +data class PairFeeResponse( + val pair:String, + val direction: String, + val userLevel: String, + val makerFee: Double, + val takerFee: Double +) \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt index 16b831307..836ea77f6 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt @@ -1,9 +1,12 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.api.core.inout.PairInfoResponse - -interface AccountantProxy { - - suspend fun getPairConfigs(): List - +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.inout.PairFeeResponse +import co.nilin.opex.api.core.inout.PairInfoResponse + +interface AccountantProxy { + + suspend fun getPairConfigs(): List + + suspend fun getFeeConfigs(): List + } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt index 8fe347450..1ccf2d514 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt @@ -1,24 +1,21 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.api.core.inout.CancelOrderRequest -import co.nilin.opex.api.core.inout.OrderSubmitResult -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import java.math.BigDecimal - -interface MEGatewayProxy { - data class CreateOrderRequest( - var uuid: String?, - val pair: String, - val price: BigDecimal, - val quantity: BigDecimal, - val direction: OrderDirection, - val matchConstraint: MatchConstraint?, - val orderType: OrderType - ) - - suspend fun createNewOrder(order: CreateOrderRequest, token: String?): OrderSubmitResult? - - suspend fun cancelOrder(request: CancelOrderRequest, token: String?): OrderSubmitResult? +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.inout.* +import java.math.BigDecimal + +interface MEGatewayProxy { + + data class CreateOrderRequest( + var uuid: String?, + val pair: String, + val price: BigDecimal, + val quantity: BigDecimal, + val direction: OrderDirection, + val matchConstraint: MatchConstraint?, + val orderType: MatchingOrderType + ) + + suspend fun createNewOrder(order: CreateOrderRequest, token: String?): OrderSubmitResult? + + suspend fun cancelOrder(request: CancelOrderRequest, token: String?): OrderSubmitResult? } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt index dc621f72b..f357d8a71 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt @@ -1,11 +1,11 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.accountant.core.inout.RichOrder -import co.nilin.opex.accountant.core.inout.RichOrderUpdate - -interface OrderPersister { - - suspend fun save(order: RichOrder) - - suspend fun update(orderUpdate: RichOrderUpdate) +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.event.RichOrder +import co.nilin.opex.api.core.event.RichOrderUpdate + +interface OrderPersister { + + suspend fun save(order: RichOrder) + + suspend fun update(orderUpdate: RichOrderUpdate) } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/SymbolMapper.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/SymbolMapper.kt index 6e36e717c..da866f989 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/SymbolMapper.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/SymbolMapper.kt @@ -4,7 +4,7 @@ interface SymbolMapper { suspend fun map(symbol: String?): String? - suspend fun unmap(value: String?): String? + suspend fun unmap(alias: String?): String? - suspend fun getKeyValues(): Map -} \ No newline at end of file + suspend fun symbolToAliasMap(): Map +} diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt index d07d91a1f..736b7858f 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt @@ -1,7 +1,7 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.accountant.core.inout.RichTrade - -interface TradePersister { - suspend fun save(trade: RichTrade) +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.event.RichTrade + +interface TradePersister { + suspend fun save(trade: RichTrade) } \ No newline at end of file diff --git a/api/api-ports/api-binance-rest/pom.xml b/api/api-ports/api-binance-rest/pom.xml index 72d6d77c8..0a046b6ee 100644 --- a/api/api-ports/api-binance-rest/pom.xml +++ b/api/api-ports/api-binance-rest/pom.xml @@ -1,105 +1,101 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - ../../pom.xml - - - co.nilin.opex.api.ports.binance - api-binance-rest - api-binance-rest - Api Binance Rest - - - - org.jetbrains.kotlin - kotlin-reflect - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.api.core - api-core - - - co.nilin.opex.utility.log - logging-handler - - - co.nilin.opex.utility.error - error-handler - - - co.nilin.opex.utility.interceptors - interceptors - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.cloud - spring-cloud-starter-consul-all - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-data-r2dbc - - - io.r2dbc - r2dbc-postgresql - runtime - - - org.postgresql - postgresql - runtime - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - - org.bouncycastle - bcprov-jdk15on - 1.60 - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - io.projectreactor - reactor-test - test - - - io.swagger - swagger-annotations - 1.5.20 - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + ../../pom.xml + + + co.nilin.opex.api.ports.binance + api-binance-rest + api-binance-rest + Api Binance Rest + + + + org.jetbrains.kotlin + kotlin-reflect + + + co.nilin.opex.api.core + api-core + + + co.nilin.opex.utility.log + logging-handler + + + co.nilin.opex.utility.error + error-handler + + + co.nilin.opex.utility.interceptors + interceptors + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-consul-all + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + io.r2dbc + r2dbc-postgresql + runtime + + + org.postgresql + postgresql + runtime + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.bouncycastle + bcprov-jdk15on + 1.60 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + io.projectreactor + reactor-test + test + + + io.swagger + swagger-annotations + 1.5.20 + + + diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt index 0a6a791b7..f8f54f5b2 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt @@ -77,6 +77,7 @@ class AccountController( @JsonInclude(JsonInclude.Include.NON_NULL) data class QueryOrderResponse( val symbol: String, + val ouid: String, val orderId: Long, val orderListId: Long, //Unless part of an OCO, the value will always be -1. val clientOrderId: String, @@ -304,6 +305,7 @@ class AccountController( return QueryOrderResponse( symbol, + response.ouid, response.orderId, response.orderListId, response.clientOrderId, @@ -363,6 +365,7 @@ class AccountController( .map { response -> QueryOrderResponse( symbol ?: "", + response.ouid, response.orderId, response.orderListId, response.clientOrderId, @@ -427,6 +430,7 @@ class AccountController( .map { response -> QueryOrderResponse( symbol ?: "", + response.ouid, response.orderId, response.orderListId, response.clientOrderId, diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/FiltersController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/FiltersController.kt index 46db1b010..a8bf2d9ca 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/FiltersController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/FiltersController.kt @@ -3,5 +3,4 @@ package co.nilin.opex.api.ports.binance.controller import org.springframework.web.bind.annotation.RestController @RestController -class FiltersController { -} \ No newline at end of file +class FiltersController diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt index 92899a607..801c16c82 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/MarketController.kt @@ -141,19 +141,20 @@ class MarketController( @RequestParam("symbols", required = false) symbols: String? ): ExchangeInfoResponse { - val symbolsMap = symbolMapper.getKeyValues() + val symbolsMap = symbolMapper.symbolToAliasMap() + val fee = accountantProxy.getFeeConfigs() val pairConfigs = accountantProxy.getPairConfigs() .map { ExchangeInfoSymbol( symbolsMap[it.pair] ?: it.pair, "TRADING", - it.leftSideWalletSymbol.toUpperCase(), + it.leftSideWalletSymbol.uppercase(), BigDecimal.valueOf(it.leftSideFraction).scale(), - it.rightSideWalletSymbol.toUpperCase(), + it.rightSideWalletSymbol.uppercase(), BigDecimal.valueOf(it.rightSideFraction).scale() ) } - return ExchangeInfoResponse(symbols = pairConfigs) + return ExchangeInfoResponse(fees = fee, symbols = pairConfigs) } // Weight(IP): 1 @@ -200,4 +201,4 @@ class MarketController( return list } -} \ No newline at end of file +} diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt index 84d49fe1e..1898f08fc 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt @@ -1,13 +1,15 @@ -package co.nilin.opex.api.ports.binance.data - -import co.nilin.opex.api.core.inout.RateLimit -import java.util.* - -data class ExchangeInfoResponse( - val timezone: String = TimeZone.getDefault().id, - val serverTime: Long = Date().time, - val rateLimits: List = RateLimit.values() - .map { RateLimitResponse(it.rateLimitType, it.interval, it.intervalNum, it.limit) }, - val exchangeFilters: List = emptyList(), - val symbols: List +package co.nilin.opex.api.ports.binance.data + +import co.nilin.opex.api.core.inout.PairFeeResponse +import co.nilin.opex.api.core.inout.RateLimit +import java.util.* + +data class ExchangeInfoResponse( + val timezone: String = TimeZone.getDefault().id, + val serverTime: Long = Date().time, + val rateLimits: List = RateLimit.values() + .map { RateLimitResponse(it.rateLimitType, it.interval, it.intervalNum, it.limit) }, + val exchangeFilters: List = emptyList(), + val fees: List = emptyList(), + val symbols: List ) \ No newline at end of file diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/proxy/AccountantProxyImpl.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/proxy/AccountantProxyImpl.kt index 4c94410b8..5e6cdb735 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/proxy/AccountantProxyImpl.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/proxy/AccountantProxyImpl.kt @@ -1,35 +1,48 @@ -package co.nilin.opex.api.ports.binance.proxy - -import co.nilin.opex.api.core.inout.PairInfoResponse -import co.nilin.opex.api.core.spi.AccountantProxy -import co.nilin.opex.api.ports.binance.util.LoggerDelegate -import kotlinx.coroutines.reactive.awaitSingle -import org.springframework.beans.factory.annotation.Value -import org.springframework.core.ParameterizedTypeReference -import org.springframework.http.MediaType -import org.springframework.stereotype.Component -import org.springframework.web.reactive.function.client.WebClient - -private inline fun typeRef(): ParameterizedTypeReference = - object : ParameterizedTypeReference() {} - -@Component -class AccountantProxyImpl(private val webClient: WebClient) : AccountantProxy { - - private val logger by LoggerDelegate() - - @Value("\${app.accountant.url}") - private lateinit var baseUrl: String - - override suspend fun getPairConfigs(): List { - logger.info("fetching pair configs") - return webClient.get() - .uri("$baseUrl/config/all") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToFlux(typeRef()) - .collectList() - .awaitSingle() - } +package co.nilin.opex.api.ports.binance.proxy + +import co.nilin.opex.api.core.inout.PairFeeResponse +import co.nilin.opex.api.core.inout.PairInfoResponse +import co.nilin.opex.api.core.spi.AccountantProxy +import co.nilin.opex.api.ports.binance.util.LoggerDelegate +import kotlinx.coroutines.reactive.awaitSingle +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +private inline fun typeRef(): ParameterizedTypeReference = + object : ParameterizedTypeReference() {} + +@Component +class AccountantProxyImpl(private val webClient: WebClient) : AccountantProxy { + + private val logger by LoggerDelegate() + + @Value("\${app.accountant.url}") + private lateinit var baseUrl: String + + override suspend fun getPairConfigs(): List { + logger.info("fetching pair configs") + return webClient.get() + .uri("$baseUrl/config/all") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToFlux(typeRef()) + .collectList() + .awaitSingle() + } + + override suspend fun getFeeConfigs(): List { + logger.info("fetching fee configs") + return webClient.get() + .uri("$baseUrl/config/fee/all") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToFlux(typeRef()) + .collectList() + .awaitSingle() + } } \ No newline at end of file diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt index 546cc78c0..fcaf4a257 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt @@ -1,36 +1,32 @@ -package co.nilin.opex.api.ports.binance.util - -import co.nilin.opex.api.core.inout.OrderSide -import co.nilin.opex.api.core.inout.TimeInForce -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType - -fun OrderSide.asOrderDirection(): OrderDirection { - if (this == OrderSide.BUY) - return OrderDirection.BID - return OrderDirection.ASK -} - -fun TimeInForce.asMatchConstraint(): MatchConstraint { - return when (this) { - TimeInForce.GTC -> MatchConstraint.GTC - TimeInForce.IOC -> MatchConstraint.IOC - TimeInForce.FOK -> MatchConstraint.FOK - } -} - -fun co.nilin.opex.api.core.inout.OrderType.asMatchingOrderType(): OrderType { - return when (this) { - co.nilin.opex.api.core.inout.OrderType.LIMIT -> OrderType.LIMIT_ORDER - co.nilin.opex.api.core.inout.OrderType.MARKET -> OrderType.MARKET_ORDER - else -> OrderType.LIMIT_ORDER - } -} - -fun > R.equalsAny(vararg equals: R): Boolean { - for (e in equals) - if (this == e) - return true - return false +package co.nilin.opex.api.ports.binance.util + +import co.nilin.opex.api.core.inout.* + +fun OrderSide.asOrderDirection(): OrderDirection { + if (this == OrderSide.BUY) + return OrderDirection.BID + return OrderDirection.ASK +} + +fun TimeInForce.asMatchConstraint(): MatchConstraint { + return when (this) { + TimeInForce.GTC -> MatchConstraint.GTC + TimeInForce.IOC -> MatchConstraint.IOC + TimeInForce.FOK -> MatchConstraint.FOK + } +} + +fun OrderType.asMatchingOrderType(): MatchingOrderType { + return when (this) { + OrderType.LIMIT -> MatchingOrderType.LIMIT_ORDER + OrderType.MARKET -> MatchingOrderType.MARKET_ORDER + else -> MatchingOrderType.LIMIT_ORDER + } +} + +fun > R.equalsAny(vararg equals: R): Boolean { + for (e in equals) + if (this == e) + return true + return false } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/pom.xml b/api/api-ports/api-eventlistener-kafka/pom.xml index 4763cdd35..51fdd04b9 100644 --- a/api/api-ports/api-eventlistener-kafka/pom.xml +++ b/api/api-ports/api-eventlistener-kafka/pom.xml @@ -1,65 +1,57 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - ../../pom.xml - - - co.nilin.opex.api.ports.kafka.listener - api-eventlistener-kafka - api-eventlistener-kafka - Api kafka listener of Opex - - - - org.jetbrains.kotlin - kotlin-reflect - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-webflux - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.accountant.core - accountant-core - - - co.nilin.opex.api.core - api-core - - - org.springframework.kafka - spring-kafka - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - org.springframework.kafka - spring-kafka-test - test - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + ../../pom.xml + + + co.nilin.opex.api.ports.kafka.listener + api-eventlistener-kafka + api-eventlistener-kafka + Api kafka listener of Opex + + + + org.jetbrains.kotlin + kotlin-reflect + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-webflux + + + co.nilin.opex.api.core + api-core + + + org.springframework.kafka + spring-kafka + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + org.springframework.kafka + spring-kafka-test + test + + + diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt index 0af4a06ce..ccf7eb01b 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt @@ -1,114 +1,93 @@ -package co.nilin.opex.api.ports.kafka.listener.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.ports.kafka.listener.consumer.EventKafkaListener -import co.nilin.opex.api.ports.kafka.listener.consumer.OrderKafkaListener -import co.nilin.opex.api.ports.kafka.listener.consumer.TradeKafkaListener -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.consumer.ConsumerConfig -import org.apache.kafka.common.TopicPartition -import org.apache.kafka.common.serialization.StringDeserializer -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.ConsumerFactory -import org.springframework.kafka.core.DefaultKafkaConsumerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.listener.* -import org.springframework.kafka.support.serializer.JsonDeserializer -import org.springframework.util.backoff.FixedBackOff -import java.util.regex.Pattern - -@Configuration -class ApiKafkaConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Value("\${spring.kafka.consumer.group-id}") - private lateinit var groupId: String - - @Bean("apiConsumerConfig") - fun consumerConfigs(): Map { - return mapOf( - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ConsumerConfig.GROUP_ID_CONFIG to groupId, - ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, - JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*", - ) - } - - @Bean("eventsConsumerFactory") - fun consumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Bean("richTradeConsumerFactory") - fun richTradeConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Bean("richOrderConsumerFactory") - fun richOrderConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Autowired - @ConditionalOnBean(TradeKafkaListener::class) - fun configureTradeListener( - tradeListener: TradeKafkaListener, - template: KafkaTemplate, - @Qualifier("richTradeConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("richTrade")) - containerProps.messageListener = tradeListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("ApiTradeKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "richTrade.DLT") - container.start() - } - - @Autowired - @ConditionalOnBean(EventKafkaListener::class) - fun configureEventListener( - eventListener: EventKafkaListener, - template: KafkaTemplate, - @Qualifier("eventsConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("events_.*")) - containerProps.messageListener = eventListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("ApiEventKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "events.DLT") - container.start() - } - - @Autowired - @ConditionalOnBean(OrderKafkaListener::class) - fun configureOrderListener( - orderListener: OrderKafkaListener, - template: KafkaTemplate, - @Qualifier("richOrderConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("richOrder")) - containerProps.messageListener = orderListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("ApiOrderKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "richOrder.DLT") - container.start() - } - - private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler { - val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ -> - cr.headers().add("dlt-origin-module", "API".toByteArray()) - TopicPartition(dltTopic, cr.partition()) - } - return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) - } - +package co.nilin.opex.api.ports.kafka.listener.config + +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.ports.kafka.listener.consumer.OrderKafkaListener +import co.nilin.opex.api.ports.kafka.listener.consumer.TradeKafkaListener +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.TopicPartition +import org.apache.kafka.common.serialization.StringDeserializer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.* +import org.springframework.kafka.support.serializer.JsonDeserializer +import org.springframework.util.backoff.FixedBackOff +import java.util.regex.Pattern + +@Configuration +class ApiKafkaConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Value("\${spring.kafka.consumer.group-id}") + private lateinit var groupId: String + + @Bean("apiConsumerConfig") + fun consumerConfigs(): Map { + return mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ConsumerConfig.GROUP_ID_CONFIG to groupId, + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, + JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*", + JsonDeserializer.TYPE_MAPPINGS to "rich_order_event:co.nilin.opex.api.core.event.RichOrderEvent,rich_order:co.nilin.opex.api.core.event.RichOrder,rich_order_update:co.nilin.opex.api.core.event.RichOrderUpdate,rich_trade:co.nilin.opex.api.core.event.RichTrade" + ) + } + + @Bean("richTradeConsumerFactory") + fun richTradeConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Bean("richOrderConsumerFactory") + fun richOrderConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Autowired + @ConditionalOnBean(TradeKafkaListener::class) + fun configureTradeListener( + tradeListener: TradeKafkaListener, + template: KafkaTemplate, + @Qualifier("richTradeConsumerFactory") consumerFactory: ConsumerFactory + ) { + val containerProps = ContainerProperties(Pattern.compile("richTrade")) + containerProps.messageListener = tradeListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.setBeanName("ApiTradeKafkaListenerContainer") + container.commonErrorHandler = createConsumerErrorHandler(template, "richTrade.DLT") + container.start() + } + + @Autowired + @ConditionalOnBean(OrderKafkaListener::class) + fun configureOrderListener( + orderListener: OrderKafkaListener, + template: KafkaTemplate, + @Qualifier("richOrderConsumerFactory") consumerFactory: ConsumerFactory + ) { + val containerProps = ContainerProperties(Pattern.compile("richOrder")) + containerProps.messageListener = orderListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.setBeanName("ApiOrderKafkaListenerContainer") + container.commonErrorHandler = createConsumerErrorHandler(template, "richOrder.DLT") + container.start() + } + + private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler { + val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ -> + cr.headers().add("dlt-origin-module", "API".toByteArray()) + TopicPartition(dltTopic, cr.partition()) + } + return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) + } + } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt index d44bbd266..ebab34a8b 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt @@ -1,63 +1,52 @@ -package co.nilin.opex.api.ports.kafka.listener.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.producer.ProducerConfig -import org.apache.kafka.common.serialization.StringSerializer -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.DefaultKafkaProducerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.core.ProducerFactory -import org.springframework.kafka.support.serializer.JsonSerializer - -@Configuration -class KafkaProducerConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Bean("apiProducerConfigs") - fun producerConfigs(): Map { - return mapOf( - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, - ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, - ProducerConfig.ACKS_CONFIG to "all" - ) - } - - @Bean("eventsProducerFactory") - fun producerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("eventKafkaTemplate") - fun kafkaTemplate(@Qualifier("eventsProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } - - @Bean("richTradeProducerFactory") - fun richTradeProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richTradeKafkaTemplate") - fun richTradeTemplate(@Qualifier("richTradeProducerFactory") factory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(factory) - } - - @Bean("richOrderProducerFactory") - fun richOrderProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richOrderKafkaTemplate") - fun richOrderTemplate(@Qualifier("richOrderProducerFactory") factory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(factory) - } - +package co.nilin.opex.api.ports.kafka.listener.config + +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.core.event.RichTrade +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.support.serializer.JsonSerializer + +@Configuration +class KafkaProducerConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Bean("apiProducerConfigs") + fun producerConfigs(): Map { + return mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, + ProducerConfig.ACKS_CONFIG to "all" + ) + } + + @Bean("richTradeProducerFactory") + fun richTradeProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richTradeKafkaTemplate") + fun richTradeTemplate(@Qualifier("richTradeProducerFactory") factory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(factory) + } + + @Bean("richOrderProducerFactory") + fun richOrderProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richOrderKafkaTemplate") + fun richOrderTemplate(@Qualifier("richOrderProducerFactory") factory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(factory) + } + } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt deleted file mode 100644 index 6c01fa606..000000000 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt +++ /dev/null @@ -1,28 +0,0 @@ -package co.nilin.opex.api.ports.kafka.listener.consumer - - -import co.nilin.opex.api.ports.kafka.listener.spi.EventListener -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class EventKafkaListener : MessageListener { - val eventListeners = arrayListOf() - override fun onMessage(data: ConsumerRecord) { - eventListeners.forEach { tl -> - tl.onEvent(data.value(), data.partition(), data.offset(), data.timestamp()) - } - } - - fun addEventListener(tl: EventListener) { - eventListeners.add(tl) - } - - fun removeEventListener(tl: EventListener) { - eventListeners.removeIf { item -> - item.id() == tl.id() - } - } -} \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt index e9492a732..e88ba3f55 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt @@ -1,29 +1,29 @@ -package co.nilin.opex.api.ports.kafka.listener.consumer - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class OrderKafkaListener : MessageListener { - - val orderListeners = arrayListOf() - - override fun onMessage(data: ConsumerRecord) { - orderListeners.forEach { tl -> - tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) - } - } - - fun addOrderListener(tl: RichOrderListener) { - orderListeners.add(tl) - } - - fun removeOrderListener(tl: RichOrderListener) { - orderListeners.removeIf { item -> - item.id() == tl.id() - } - } +package co.nilin.opex.api.ports.kafka.listener.consumer + +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.listener.MessageListener +import org.springframework.stereotype.Component + +@Component +class OrderKafkaListener : MessageListener { + + val orderListeners = arrayListOf() + + override fun onMessage(data: ConsumerRecord) { + orderListeners.forEach { tl -> + tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) + } + } + + fun addOrderListener(tl: RichOrderListener) { + orderListeners.add(tl) + } + + fun removeOrderListener(tl: RichOrderListener) { + orderListeners.removeIf { item -> + item.id() == tl.id() + } + } } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt index ace560b20..024771d80 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt @@ -1,27 +1,29 @@ -package co.nilin.opex.api.ports.kafka.listener.consumer - -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class TradeKafkaListener : MessageListener { - val tradeListeners = arrayListOf() - override fun onMessage(data: ConsumerRecord) { - tradeListeners.forEach { tl -> - tl.onTrade(data.value(), data.partition(), data.offset(), data.timestamp()) - } - } - - fun addTradeListener(tl: RichTradeListener) { - tradeListeners.add(tl) - } - - fun removeTradeListener(tl: RichTradeListener) { - tradeListeners.removeIf { item -> - item.id() == tl.id() - } - } +package co.nilin.opex.api.ports.kafka.listener.consumer + +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.listener.MessageListener +import org.springframework.stereotype.Component + +@Component +class TradeKafkaListener : MessageListener { + + val tradeListeners = arrayListOf() + + override fun onMessage(data: ConsumerRecord) { + tradeListeners.forEach { tl -> + tl.onTrade(data.value(), data.partition(), data.offset(), data.timestamp()) + } + } + + fun addTradeListener(tl: RichTradeListener) { + tradeListeners.add(tl) + } + + fun removeTradeListener(tl: RichTradeListener) { + tradeListeners.removeIf { item -> + item.id() == tl.id() + } + } } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt deleted file mode 100644 index e98cc1212..000000000 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.nilin.opex.api.ports.kafka.listener.spi - -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent - - -interface EventListener { - fun id(): String - fun onEvent(coreEvent: CoreEvent, partition: Int, offset: Long, timestamp: Long) -} \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt index a87a85c5b..8809dced7 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt @@ -1,11 +1,11 @@ -package co.nilin.opex.api.ports.kafka.listener.spi - -import co.nilin.opex.accountant.core.inout.RichOrderEvent - -interface RichOrderListener { - - fun id(): String - - fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) - +package co.nilin.opex.api.ports.kafka.listener.spi + +import co.nilin.opex.api.core.event.RichOrderEvent + +interface RichOrderListener { + + fun id(): String + + fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) + } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt index de14026e7..c852f831e 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt @@ -1,8 +1,8 @@ -package co.nilin.opex.api.ports.kafka.listener.spi - -import co.nilin.opex.accountant.core.inout.RichTrade - -interface RichTradeListener { - fun id(): String - fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) +package co.nilin.opex.api.ports.kafka.listener.spi + +import co.nilin.opex.api.core.event.RichTrade + +interface RichTradeListener { + fun id(): String + fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/pom.xml b/api/api-ports/api-persister-postgres/pom.xml index d00fff280..28ded2b84 100644 --- a/api/api-ports/api-persister-postgres/pom.xml +++ b/api/api-ports/api-persister-postgres/pom.xml @@ -1,75 +1,67 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - ../../pom.xml - - - co.nilin.opex.api.ports.postgres - api-persister-postgres - api-persister-postgres - Persist items of Opex api on Postgres - - - - org.jetbrains.kotlin - kotlin-reflect - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.api.core - api-core - - - co.nilin.opex.accountant.core - accountant-core - - - co.nilin.opex.utility.error - error-handler - - - org.springframework.boot - spring-boot-starter-data-r2dbc - - - io.r2dbc - r2dbc-postgresql - runtime - - - org.postgresql - postgresql - runtime - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - com.google.code.gson - gson - - - io.projectreactor - reactor-test - test - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + ../../pom.xml + + + co.nilin.opex.api.ports.postgres + api-persister-postgres + api-persister-postgres + Persist items of Opex api on Postgres + + + + org.jetbrains.kotlin + kotlin-reflect + + + co.nilin.opex.api.core + api-core + + + co.nilin.opex.utility.error + error-handler + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + io.r2dbc + r2dbc-postgresql + runtime + + + org.postgresql + postgresql + runtime + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + com.google.code.gson + gson + + + io.projectreactor + reactor-test + test + + + diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt index a2924ed23..47e7038ae 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt @@ -10,17 +10,13 @@ import org.springframework.r2dbc.core.DatabaseClient @EnableR2dbcRepositories(basePackages = ["co.nilin.opex"]) class PostgresConfig( db: DatabaseClient, - @Value("classpath:schema.sql") private val schemaResource: Resource, - @Value("classpath:data.sql") private val dataResource: Resource? + @Value("classpath:schema.sql") private val schemaResource: Resource ) { init { val schemaReader = schemaResource.inputStream.reader() val schema = schemaReader.readText().trim() schemaReader.close() - val dataReader = dataResource?.inputStream?.reader() - val data = dataReader?.readText()?.trim() ?: "" - dataReader?.close() - val initDb = db.sql { schema.plus(data) } + val initDb = db.sql { schema } initDb // initialize the database .then() .subscribe() // execute diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt index 6dd73ce68..1a2f0c57d 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt @@ -1,118 +1,118 @@ -package co.nilin.opex.api.ports.postgres.dao - -import co.nilin.opex.api.core.inout.AggregatedOrderPriceModel -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.matching.engine.core.model.OrderDirection -import kotlinx.coroutines.flow.Flow -import org.springframework.data.r2dbc.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import java.util.* - -@Repository -interface OrderRepository : ReactiveCrudRepository { - - @Query("select * from orders where ouid = :ouid") - fun findByOuid(@Param("ouid") ouid: String): Mono - - @Query("select * from orders where symbol = :symbol and order_id = :orderId") - fun findBySymbolAndOrderId( - @Param("symbol") - symbol: String, @Param("orderId") - orderId: Long - ): Mono - - @Query("select * from orders where symbol = :symbol and client_order_id = :origClientOrderId") - fun findBySymbolAndClientOrderId( - @Param("symbol") - symbol: String, - @Param("origClientOrderId") - origClientOrderId: String - ): Mono - - @Query( - """ - select * from orders - join order_status os on orders.ouid = os.ouid - where uuid = :uuid and (:symbol is null or symbol = :symbol) and status in (:statuses) - and appearance = (select max(appearance) from order_status where ouid = orders.ouid) - and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) - """ - ) - fun findByUuidAndSymbolAndStatus( - @Param("uuid") - uuid: String, - @Param("symbol") - symbol: String?, @Param("statuses") - status: Collection - ): Flow - - @Query( - "select * from orders where uuid = :uuid " + - "and (:symbol is null or symbol = :symbol) " + - "and (:startTime is null or update_date >= :startTime)" + - "and (:endTime is null or update_date < :endTime)" - ) - fun findByUuidAndSymbolAndTimeBetween( - @Param("uuid") - uuid: String, - @Param("symbol") - symbol: String?, - @Param("startTime") - startTime: Date?, - @Param("endTime") - endTime: Date? - ): Flow - - @Query( - """ - select price, (sum(quantity) - sum(os.executed_quantity)) as quantity from orders - join order_status os on orders.ouid = os.ouid - where symbol = :symbol and side = :direction and os.status in (:statuses) - and appearance = (select max(appearance) from order_status where ouid = orders.ouid) - and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) - group by price - order by price asc - limit :limit - """ - ) - fun findBySymbolAndDirectionAndStatusSortAscendingByPrice( - @Param("symbol") - symbol: String, - @Param("direction") - direction: OrderDirection, - @Param("limit") - limit: Int, - @Param("statuses") - status: Collection - ): Flux - - @Query( - """ - select price, (sum(quantity) - sum(executed_quantity)) as quantity from orders - join order_status os on orders.ouid = os.ouid - where symbol = :symbol and side = :direction and status in (:statuses) - and appearance = (select max(appearance) from order_status where ouid = orders.ouid) - and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) - group by price - order by price desc - limit :limit - """ - ) - fun findBySymbolAndDirectionAndStatusSortDescendingByPrice( - @Param("symbol") - symbol: String, - @Param("direction") - direction: OrderDirection, - @Param("limit") - limit: Int, - @Param("statuses") - status: Collection - ): Flux - - @Query("select * from orders where symbol = :symbol order by create_date desc limit 1") - fun findLastOrderBySymbol(@Param("symbol") symbol: String): Mono +package co.nilin.opex.api.ports.postgres.dao + +import co.nilin.opex.api.core.inout.AggregatedOrderPriceModel +import co.nilin.opex.api.core.inout.OrderDirection +import co.nilin.opex.api.ports.postgres.model.OrderModel +import kotlinx.coroutines.flow.Flow +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.* + +@Repository +interface OrderRepository : ReactiveCrudRepository { + + @Query("select * from orders where ouid = :ouid") + fun findByOuid(@Param("ouid") ouid: String): Mono + + @Query("select * from orders where symbol = :symbol and order_id = :orderId") + fun findBySymbolAndOrderId( + @Param("symbol") + symbol: String, @Param("orderId") + orderId: Long + ): Mono + + @Query("select * from orders where symbol = :symbol and client_order_id = :origClientOrderId") + fun findBySymbolAndClientOrderId( + @Param("symbol") + symbol: String, + @Param("origClientOrderId") + origClientOrderId: String + ): Mono + + @Query( + """ + select * from orders + join order_status os on orders.ouid = os.ouid + where uuid = :uuid and (:symbol is null or symbol = :symbol) and status in (:statuses) + and appearance = (select max(appearance) from order_status where ouid = orders.ouid) + and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + """ + ) + fun findByUuidAndSymbolAndStatus( + @Param("uuid") + uuid: String, + @Param("symbol") + symbol: String?, @Param("statuses") + status: Collection + ): Flow + + @Query( + "select * from orders where uuid = :uuid " + + "and (:symbol is null or symbol = :symbol) " + + "and (:startTime is null or update_date >= :startTime)" + + "and (:endTime is null or update_date < :endTime)" + ) + fun findByUuidAndSymbolAndTimeBetween( + @Param("uuid") + uuid: String, + @Param("symbol") + symbol: String?, + @Param("startTime") + startTime: Date?, + @Param("endTime") + endTime: Date? + ): Flow + + @Query( + """ + select price, (sum(quantity) - sum(os.executed_quantity)) as quantity from orders + join order_status os on orders.ouid = os.ouid + where symbol = :symbol and side = :direction and os.status in (:statuses) + and appearance = (select max(appearance) from order_status where ouid = orders.ouid) + and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + group by price + order by price asc + limit :limit + """ + ) + fun findBySymbolAndDirectionAndStatusSortAscendingByPrice( + @Param("symbol") + symbol: String, + @Param("direction") + direction: OrderDirection, + @Param("limit") + limit: Int, + @Param("statuses") + status: Collection + ): Flux + + @Query( + """ + select price, (sum(quantity) - sum(executed_quantity)) as quantity from orders + join order_status os on orders.ouid = os.ouid + where symbol = :symbol and side = :direction and status in (:statuses) + and appearance = (select max(appearance) from order_status where ouid = orders.ouid) + and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + group by price + order by price desc + limit :limit + """ + ) + fun findBySymbolAndDirectionAndStatusSortDescendingByPrice( + @Param("symbol") + symbol: String, + @Param("direction") + direction: OrderDirection, + @Param("limit") + limit: Int, + @Param("statuses") + status: Collection + ): Flux + + @Query("select * from orders where symbol = :symbol order by create_date desc limit 1") + fun findLastOrderBySymbol(@Param("symbol") symbol: String): Mono } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/SymbolMapRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/SymbolMapRepository.kt index 4bc30a34a..66c478e2f 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/SymbolMapRepository.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/SymbolMapRepository.kt @@ -9,10 +9,9 @@ import reactor.core.publisher.Mono @Repository interface SymbolMapRepository : ReactiveCrudRepository { + @Query("select * from symbol_maps where symbol = :symbol and alias_key = :aliasKey") + fun findByAliasKeyAndSymbol(aliasKey: String, @Param("symbol") symbol: String): Mono - @Query("select * from symbol_maps where symbol = :symbol") - fun findBySymbol(@Param("symbol") symbol: String): Mono - - @Query("select * from symbol_maps where value = :value") - fun findByValue(@Param("value") value: String): Mono -} \ No newline at end of file + @Query("select * from symbol_maps where alias_key = :aliasKey and alias = :alias") + fun findByAliasKeyAndAlias(aliasKey: String, @Param("alias") alias: String): Mono +} diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt index 4f87631b7..19ced19fd 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt @@ -1,216 +1,215 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.api.core.inout.* -import co.nilin.opex.api.core.spi.MarketQueryHandler -import co.nilin.opex.api.core.spi.SymbolMapper -import co.nilin.opex.api.ports.postgres.dao.OrderRepository -import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository -import co.nilin.opex.api.ports.postgres.dao.TradeRepository -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.api.ports.postgres.model.OrderStatusModel -import co.nilin.opex.api.ports.postgres.model.TradeTickerData -import co.nilin.opex.api.ports.postgres.util.* -import co.nilin.opex.matching.engine.core.model.OrderDirection -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Component -import java.math.BigDecimal -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.util.* -import kotlin.math.max -import kotlin.math.min - -@Component -class MarketQueryHandlerImpl( - private val orderRepository: OrderRepository, - private val tradeRepository: TradeRepository, - private val orderStatusRepository: OrderStatusRepository, - private val symbolMapper: SymbolMapper, -) : MarketQueryHandler { - - override suspend fun getTradeTickerData(startFrom: LocalDateTime): List { - return tradeRepository.tradeTicker(startFrom) - .collectList() - .awaitFirstOrElse { emptyList() } - .map { it.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) } - - } - - override suspend fun getTradeTickerDataBySymbol(symbol: String, startFrom: LocalDateTime): PriceChangeResponse { - return tradeRepository.tradeTickerBySymbol(symbol, startFrom) - .awaitFirstOrNull() - ?.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) - ?: PriceChangeResponse( - symbol = symbol, - openTime = Date().time, - closeTime = startFrom.toInstant(ZoneOffset.UTC).toEpochMilli() - ) - } - - override suspend fun openBidOrders(symbol: String, limit: Int): List { - return orderRepository.findBySymbolAndDirectionAndStatusSortDescendingByPrice( - symbol, - OrderDirection.BID, - limit, - listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) - ).collectList() - .awaitFirstOrElse { emptyList() } - .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } - } - - override suspend fun openAskOrders(symbol: String, limit: Int): List { - return orderRepository.findBySymbolAndDirectionAndStatusSortAscendingByPrice( - symbol, - OrderDirection.ASK, - limit, - listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) - ).collectList() - .awaitFirstOrElse { emptyList() } - .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } - } - - override suspend fun lastOrder(symbol: String): QueryOrderResponse? { - val order = orderRepository.findLastOrderBySymbol(symbol).awaitFirstOrNull() ?: return null - val status = orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull() - return order.asQueryOrderResponse(status) - } - - override suspend fun recentTrades(symbol: String, limit: Int): Flow { - return tradeRepository.findBySymbolSortDescendingByCreateDate(symbol, limit) - .map { - val takerOrder = orderRepository.findByOuid(it.takerOuid).awaitFirst() - val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() - val isMakerBuyer = makerOrder.direction == OrderDirection.BID - MarketTradeResponse( - it.symbol, - it.tradeId, - if (isMakerBuyer) it.makerPrice.toBigDecimal() else it.takerPrice.toBigDecimal(), - it.matchedQuantity.toBigDecimal(), - if (isMakerBuyer) - makerOrder.quoteQuantity!!.toBigDecimal() - else - takerOrder.quoteQuantity!!.toBigDecimal(), - Date.from(it.createDate.atZone(ZoneId.systemDefault()).toInstant()), - true, - isMakerBuyer - ) - } - } - - override suspend fun lastPrice(symbol: String?): List { - val list = if (symbol.isNullOrEmpty()) - tradeRepository.findAllGroupBySymbol() - else - tradeRepository.findBySymbolGroupBySymbol(symbol) - return list.collectList() - .awaitFirstOrElse { emptyList() } - .map { - val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() - val apiSymbol = try { - symbolMapper.map(it.symbol) - } catch (e: Exception) { - it.symbol - } - val isMakerBuyer = makerOrder.direction == OrderDirection.BID - PriceTickerResponse( - apiSymbol, - if (isMakerBuyer) - min(it.takerPrice, it.makerPrice).toString() - else - max(it.takerPrice, it.makerPrice).toString() - ) - } - - } - - override suspend fun getCandleInfo( - symbol: String, - interval: String, - startTime: Long?, - endTime: Long?, - limit: Int - ): List { - val st = if (startTime == null) - tradeRepository.findFirstByCreateDate().awaitFirstOrNull()?.createDate - ?: LocalDateTime.now() - else - with(Instant.ofEpochMilli(startTime)) { - LocalDateTime.ofInstant(this, ZoneId.systemDefault()) - } - - val et = if (endTime == null) - tradeRepository.findLastByCreateDate().awaitFirstOrNull()?.createDate - ?: LocalDateTime.now() - else - with(Instant.ofEpochMilli(endTime)) { - LocalDateTime.ofInstant(this, ZoneId.systemDefault()) - } - - return tradeRepository.candleData(symbol, interval, st, et, limit) - .collectList() - .awaitFirstOrElse { emptyList() } - .map { - CandleData( - it.openTime, - it.closeTime, - it.open ?: 0.0, - it.close ?: 0.0, - it.high ?: 0.0, - it.low ?: 0.0, - it.volume ?: 0.0, - 0.0, - it.trades, - 0.0, - 0.0 - ) - } - } - - private fun OrderModel.asQueryOrderResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( - symbol, - ouid, - orderId ?: -1, - -1, - clientOrderId ?: "", - price!!.toBigDecimal(), - quantity!!.toBigDecimal(), - orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, - constraint!!.toTimeInForce(), - type!!.toApiOrderType(), - direction!!.toOrderSide(), - null, - null, - Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), - Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), - (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), - quoteQuantity!!.toBigDecimal() - ) - - private fun TradeTickerData.asPriceChangeResponse(openTime: Long, closeTime: Long) = PriceChangeResponse( - symbol, - priceChange ?: 0.0, - priceChangePercent ?: 0.0, - weightedAvgPrice ?: 0.0, - lastPrice ?: 0.0, - lastQty ?: 0.0, - bidPrice ?: 0.0, - askPrice ?: 0.0, - openPrice ?: 0.0, - highPrice ?: 0.0, - lowPrice ?: 0.0, - volume ?: 0.0, - openTime, - closeTime, - firstId ?: -1, - lastId ?: -1, - count ?: 0 - ) +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.inout.* +import co.nilin.opex.api.core.spi.MarketQueryHandler +import co.nilin.opex.api.core.spi.SymbolMapper +import co.nilin.opex.api.ports.postgres.dao.OrderRepository +import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository +import co.nilin.opex.api.ports.postgres.dao.TradeRepository +import co.nilin.opex.api.ports.postgres.model.OrderModel +import co.nilin.opex.api.ports.postgres.model.OrderStatusModel +import co.nilin.opex.api.ports.postgres.model.TradeTickerData +import co.nilin.opex.api.ports.postgres.util.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.util.* +import kotlin.math.max +import kotlin.math.min + +@Component +class MarketQueryHandlerImpl( + private val orderRepository: OrderRepository, + private val tradeRepository: TradeRepository, + private val orderStatusRepository: OrderStatusRepository, + private val symbolMapper: SymbolMapper, +) : MarketQueryHandler { + + override suspend fun getTradeTickerData(startFrom: LocalDateTime): List { + return tradeRepository.tradeTicker(startFrom) + .collectList() + .awaitFirstOrElse { emptyList() } + .map { it.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) } + + } + + override suspend fun getTradeTickerDataBySymbol(symbol: String, startFrom: LocalDateTime): PriceChangeResponse { + return tradeRepository.tradeTickerBySymbol(symbol, startFrom) + .awaitFirstOrNull() + ?.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) + ?: PriceChangeResponse( + symbol = symbol, + openTime = Date().time, + closeTime = startFrom.toInstant(ZoneOffset.UTC).toEpochMilli() + ) + } + + override suspend fun openBidOrders(symbol: String, limit: Int): List { + return orderRepository.findBySymbolAndDirectionAndStatusSortDescendingByPrice( + symbol, + OrderDirection.BID, + limit, + listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) + ).collectList() + .awaitFirstOrElse { emptyList() } + .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } + } + + override suspend fun openAskOrders(symbol: String, limit: Int): List { + return orderRepository.findBySymbolAndDirectionAndStatusSortAscendingByPrice( + symbol, + OrderDirection.ASK, + limit, + listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) + ).collectList() + .awaitFirstOrElse { emptyList() } + .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } + } + + override suspend fun lastOrder(symbol: String): QueryOrderResponse? { + val order = orderRepository.findLastOrderBySymbol(symbol).awaitFirstOrNull() ?: return null + val status = orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull() + return order.asQueryOrderResponse(status) + } + + override suspend fun recentTrades(symbol: String, limit: Int): Flow { + return tradeRepository.findBySymbolSortDescendingByCreateDate(symbol, limit) + .map { + val takerOrder = orderRepository.findByOuid(it.takerOuid).awaitFirst() + val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + MarketTradeResponse( + it.symbol, + it.tradeId, + if (isMakerBuyer) it.makerPrice.toBigDecimal() else it.takerPrice.toBigDecimal(), + it.matchedQuantity.toBigDecimal(), + if (isMakerBuyer) + makerOrder.quoteQuantity!!.toBigDecimal() + else + takerOrder.quoteQuantity!!.toBigDecimal(), + Date.from(it.createDate.atZone(ZoneId.systemDefault()).toInstant()), + true, + isMakerBuyer + ) + } + } + + override suspend fun lastPrice(symbol: String?): List { + val list = if (symbol.isNullOrEmpty()) + tradeRepository.findAllGroupBySymbol() + else + tradeRepository.findBySymbolGroupBySymbol(symbol) + return list.collectList() + .awaitFirstOrElse { emptyList() } + .map { + val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() + val apiSymbol = try { + symbolMapper.map(it.symbol) + } catch (e: Exception) { + it.symbol + } + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + PriceTickerResponse( + apiSymbol, + if (isMakerBuyer) + min(it.takerPrice, it.makerPrice).toString() + else + max(it.takerPrice, it.makerPrice).toString() + ) + } + + } + + override suspend fun getCandleInfo( + symbol: String, + interval: String, + startTime: Long?, + endTime: Long?, + limit: Int + ): List { + val st = if (startTime == null) + tradeRepository.findFirstByCreateDate().awaitFirstOrNull()?.createDate + ?: LocalDateTime.now() + else + with(Instant.ofEpochMilli(startTime)) { + LocalDateTime.ofInstant(this, ZoneId.systemDefault()) + } + + val et = if (endTime == null) + tradeRepository.findLastByCreateDate().awaitFirstOrNull()?.createDate + ?: LocalDateTime.now() + else + with(Instant.ofEpochMilli(endTime)) { + LocalDateTime.ofInstant(this, ZoneId.systemDefault()) + } + + return tradeRepository.candleData(symbol, interval, st, et, limit) + .collectList() + .awaitFirstOrElse { emptyList() } + .map { + CandleData( + it.openTime, + it.closeTime, + it.open ?: 0.0, + it.close ?: 0.0, + it.high ?: 0.0, + it.low ?: 0.0, + it.volume ?: 0.0, + 0.0, + it.trades, + 0.0, + 0.0 + ) + } + } + + private fun OrderModel.asQueryOrderResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( + symbol, + ouid, + orderId ?: -1, + -1, + clientOrderId ?: "", + price!!.toBigDecimal(), + quantity!!.toBigDecimal(), + orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, + constraint!!.toTimeInForce(), + type!!.toApiOrderType(), + direction!!.toOrderSide(), + null, + null, + Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), + Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), + (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), + quoteQuantity!!.toBigDecimal() + ) + + private fun TradeTickerData.asPriceChangeResponse(openTime: Long, closeTime: Long) = PriceChangeResponse( + symbol, + priceChange ?: 0.0, + priceChangePercent ?: 0.0, + weightedAvgPrice ?: 0.0, + lastPrice ?: 0.0, + lastQty ?: 0.0, + bidPrice ?: 0.0, + askPrice ?: 0.0, + openPrice ?: 0.0, + highPrice ?: 0.0, + lowPrice ?: 0.0, + volume ?: 0.0, + openTime, + closeTime, + firstId ?: -1, + lastId ?: -1, + count ?: 0 + ) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt index 45b7f1049..cc520af06 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt @@ -1,74 +1,78 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.accountant.core.inout.OrderStatus -import co.nilin.opex.accountant.core.inout.RichOrder -import co.nilin.opex.accountant.core.inout.RichOrderUpdate -import co.nilin.opex.api.core.spi.OrderPersister -import co.nilin.opex.api.ports.postgres.dao.OrderRepository -import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.api.ports.postgres.model.OrderStatusModel -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import java.time.LocalDateTime - -@Component -class OrderPersisterImpl( - private val orderRepository: OrderRepository, - private val orderStatusRepository: OrderStatusRepository -) : OrderPersister { - - private val logger = LoggerFactory.getLogger(OrderPersisterImpl::class.java) - - override suspend fun save(order: RichOrder) { - orderRepository.save( - OrderModel( - null, - order.ouid, - order.uuid, - null, - order.pair, - order.orderId, - order.makerFee.toDouble(), - order.takerFee.toDouble(), - order.leftSideFraction.toDouble(), - order.rightSideFraction.toDouble(), - order.userLevel, - order.direction, - order.constraint, - order.type, - order.price.toDouble(), - order.quantity.toDouble(), - order.quoteQuantity.toDouble(), - LocalDateTime.now(), - LocalDateTime.now() - ) - ).awaitFirstOrNull() - logger.info("order ${order.ouid} saved") - - orderStatusRepository.save( - OrderStatusModel( - order.ouid, - order.executedQuantity.toDouble(), - order.accumulativeQuoteQty.toDouble(), - OrderStatus.NEW.code, - OrderStatus.NEW.orderOfAppearance - ) - ).awaitFirstOrNull() - logger.info("OrderStatus ${order.ouid} saved with status of 'NEW'") - } - - override suspend fun update(orderUpdate: RichOrderUpdate) { - orderStatusRepository.save( - OrderStatusModel( - orderUpdate.ouid, - orderUpdate.executedQuantity().toDouble(), - orderUpdate.accumulativeQuoteQuantity().toDouble(), - orderUpdate.status.code, - orderUpdate.status.orderOfAppearance - ) - ).awaitFirstOrNull() - logger.info("OrderStatus ${orderUpdate.ouid} updated with status of ${orderUpdate.status}") - } +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.event.RichOrder +import co.nilin.opex.api.core.event.RichOrderUpdate +import co.nilin.opex.api.core.inout.OrderStatus +import co.nilin.opex.api.core.spi.OrderPersister +import co.nilin.opex.api.ports.postgres.dao.OrderRepository +import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository +import co.nilin.opex.api.ports.postgres.model.OrderModel +import co.nilin.opex.api.ports.postgres.model.OrderStatusModel +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class OrderPersisterImpl( + private val orderRepository: OrderRepository, + private val orderStatusRepository: OrderStatusRepository +) : OrderPersister { + + private val logger = LoggerFactory.getLogger(OrderPersisterImpl::class.java) + + override suspend fun save(order: RichOrder) { + orderRepository.save( + OrderModel( + null, + order.ouid, + order.uuid, + null, + order.pair, + order.orderId, + order.makerFee.toDouble(), + order.takerFee.toDouble(), + order.leftSideFraction.toDouble(), + order.rightSideFraction.toDouble(), + order.userLevel, + order.direction, + order.constraint, + order.type, + order.price.toDouble(), + order.quantity.toDouble(), + order.quoteQuantity.toDouble(), + LocalDateTime.now(), + LocalDateTime.now() + ) + ).awaitFirstOrNull() + logger.info("order ${order.ouid} saved") + + orderStatusRepository.save( + OrderStatusModel( + order.ouid, + order.executedQuantity.toDouble(), + order.accumulativeQuoteQty.toDouble(), + OrderStatus.NEW.code, + OrderStatus.NEW.orderOfAppearance + ) + ).awaitFirstOrNull() + logger.info("OrderStatus ${order.ouid} saved with status of 'NEW'") + } + + override suspend fun update(orderUpdate: RichOrderUpdate) { + try { + orderStatusRepository.save( + OrderStatusModel( + orderUpdate.ouid, + orderUpdate.executedQuantity().toDouble(), + orderUpdate.accumulativeQuoteQuantity().toDouble(), + orderUpdate.status.code, + orderUpdate.status.orderOfAppearance + ) + ).awaitFirstOrNull() + } catch (e: Exception) { + logger.error("Error updating order status: ${e.message}") + } + logger.info("OrderStatus ${orderUpdate.ouid} updated with status of ${orderUpdate.status}") + } } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperImpl.kt index d8f462be7..b635a2812 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperImpl.kt @@ -11,20 +11,19 @@ class SymbolMapperImpl(val symbolMapRepository: SymbolMapRepository) : SymbolMap override suspend fun map(symbol: String?): String? { if (symbol == null) return null - return symbolMapRepository.findBySymbol(symbol).awaitFirstOrNull()?.value + return symbolMapRepository.findByAliasKeyAndSymbol("binance", symbol).awaitFirstOrNull()?.alias } - override suspend fun unmap(value: String?): String? { - if (value == null) return null - return symbolMapRepository.findByValue(value).awaitFirstOrNull()?.symbol + override suspend fun unmap(alias: String?): String? { + if (alias == null) return null + return symbolMapRepository.findByAliasKeyAndAlias("binance", alias).awaitFirstOrNull()?.symbol } - override suspend fun getKeyValues(): Map { - val map = HashMap() - symbolMapRepository.findAll() + override suspend fun symbolToAliasMap(): Map { + return symbolMapRepository.findAll() + .filter { it.aliasKey == "binance" } .collectList() .awaitFirstOrElse { emptyList() } - .forEach { map[it.symbol] = it.value } - return map + .associate { it.symbol to it.alias } } -} \ No newline at end of file +} diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt index 309cf9488..019c00df4 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt @@ -1,42 +1,42 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.core.spi.TradePersister -import co.nilin.opex.api.ports.postgres.dao.TradeRepository -import co.nilin.opex.api.ports.postgres.model.TradeModel -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime - -@Component -class TradePersisterImpl(private val tradeRepository: TradeRepository) : TradePersister { - - private val logger = LoggerFactory.getLogger(TradePersisterImpl::class.java) - - @Transactional - override suspend fun save(trade: RichTrade) { - tradeRepository.save( - TradeModel( - null, - trade.id, - trade.pair, - trade.matchedQuantity.toDouble(), - trade.takerPrice.toDouble(), - trade.makerPrice.toDouble(), - trade.takerCommision.toDouble(), - trade.makerCommision.toDouble(), - trade.takerCommisionAsset, - trade.makerCommisionAsset, - trade.tradeDateTime, - trade.makerOuid, - trade.takerOuid, - trade.makerUuid, - trade.takerUuid, - LocalDateTime.now() - ) - ).awaitFirstOrNull() - logger.info("RichTrade ${trade.id} saved") - } +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.core.spi.TradePersister +import co.nilin.opex.api.ports.postgres.dao.TradeRepository +import co.nilin.opex.api.ports.postgres.model.TradeModel +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Component +class TradePersisterImpl(private val tradeRepository: TradeRepository) : TradePersister { + + private val logger = LoggerFactory.getLogger(TradePersisterImpl::class.java) + + @Transactional + override suspend fun save(trade: RichTrade) { + tradeRepository.save( + TradeModel( + null, + trade.id, + trade.pair, + trade.matchedQuantity.toDouble(), + trade.takerPrice.toDouble(), + trade.makerPrice.toDouble(), + trade.takerCommision.toDouble(), + trade.makerCommision.toDouble(), + trade.takerCommisionAsset, + trade.makerCommisionAsset, + trade.tradeDateTime, + trade.makerOuid, + trade.takerOuid, + trade.makerUuid, + trade.takerUuid, + LocalDateTime.now() + ) + ).awaitFirstOrNull() + logger.info("RichTrade ${trade.id} saved") + } } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt index 8088bc2fc..656335de5 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt @@ -1,139 +1,138 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.api.core.inout.* -import co.nilin.opex.api.core.spi.UserQueryHandler -import co.nilin.opex.api.ports.postgres.dao.OrderRepository -import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository -import co.nilin.opex.api.ports.postgres.dao.TradeRepository -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.api.ports.postgres.model.OrderStatusModel -import co.nilin.opex.api.ports.postgres.util.* -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.OpexException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Component -import java.math.BigDecimal -import java.security.Principal -import java.time.ZoneId -import java.util.* - -@Component -class UserQueryHandlerImpl( - private val orderRepository: OrderRepository, - private val tradeRepository: TradeRepository, - private val orderStatusRepository: OrderStatusRepository -) : UserQueryHandler { - - override suspend fun queryOrder(principal: Principal, request: QueryOrderRequest): QueryOrderResponse? { - val order = (if (request.origClientOrderId != null) { - orderRepository.findBySymbolAndClientOrderId(request.symbol, request.origClientOrderId!!) - } else { - orderRepository.findBySymbolAndOrderId(request.symbol, request.orderId!!) - - }).awaitFirstOrNull() ?: return null - - if (order.uuid != principal.name) - throw OpexException(OpexError.Forbidden) - - return order.asQueryResponse(orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull()) - } - - override suspend fun openOrders(principal: Principal, symbol: String?): Flow { - return orderRepository.findByUuidAndSymbolAndStatus( - principal.name, - symbol, - listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) - ).filter { orderModel -> orderModel.constraint != null } - .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } - } - - override suspend fun allOrders(principal: Principal, allOrderRequest: AllOrderRequest): Flow { - return orderRepository.findByUuidAndSymbolAndTimeBetween( - principal.name, - allOrderRequest.symbol, - allOrderRequest.startTime, - allOrderRequest.endTime - ).filter { orderModel -> orderModel.constraint != null } - .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } - } - - override suspend fun allTrades(principal: Principal, request: TradeRequest): Flow { - return tradeRepository.findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan( - principal.name, request.symbol, request.fromTrade, request.startTime, request.endTime - ).map { trade -> - val takerOrder = orderRepository.findByOuid(trade.takerOuid).awaitFirst() - val makerOrder = orderRepository.findByOuid(trade.makerOuid).awaitFirst() - val isMakerBuyer = makerOrder.direction == OrderDirection.BID - TradeResponse( - trade.symbol, - trade.tradeId, - if (trade.takerUuid == principal.name) { - takerOrder.orderId!! - } else { - makerOrder.orderId!! - }, - -1, - if (trade.takerUuid == principal.name) { - trade.takerPrice.toBigDecimal() - } else { - trade.makerPrice.toBigDecimal() - }, - trade.matchedQuantity.toBigDecimal(), - if (isMakerBuyer) { - makerOrder.quoteQuantity!!.toBigDecimal() - } else { - takerOrder.quoteQuantity!!.toBigDecimal() - }, - if (trade.takerUuid == principal.name) { - trade.takerCommision!!.toBigDecimal() - } else { - trade.makerCommision!!.toBigDecimal() - }, - if (trade.takerUuid == principal.name) { - trade.takerCommisionAsset!! - } else { - trade.makerCommisionAsset!! - }, - Date.from( - trade.createDate.atZone(ZoneId.systemDefault()).toInstant() - ), - if (trade.takerUuid == principal.name) { - OrderDirection.ASK == takerOrder.direction - } else { - OrderDirection.ASK == makerOrder.direction - }, - trade.makerUuid == principal.name, - true, - isMakerBuyer - ) - } - } - - - private fun OrderModel.asQueryResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( - symbol, - ouid, - orderId ?: -1, - -1, - clientOrderId ?: "", - price!!.toBigDecimal(), - quantity!!.toBigDecimal(), - orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, - constraint!!.toTimeInForce(), - type!!.toApiOrderType(), - direction!!.toOrderSide(), - null, - null, - Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), - Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), - (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), - quoteQuantity!!.toBigDecimal() - ) +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.inout.* +import co.nilin.opex.api.core.spi.UserQueryHandler +import co.nilin.opex.api.ports.postgres.dao.OrderRepository +import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository +import co.nilin.opex.api.ports.postgres.dao.TradeRepository +import co.nilin.opex.api.ports.postgres.model.OrderModel +import co.nilin.opex.api.ports.postgres.model.OrderStatusModel +import co.nilin.opex.api.ports.postgres.util.* +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.security.Principal +import java.time.ZoneId +import java.util.* + +@Component +class UserQueryHandlerImpl( + private val orderRepository: OrderRepository, + private val tradeRepository: TradeRepository, + private val orderStatusRepository: OrderStatusRepository +) : UserQueryHandler { + + override suspend fun queryOrder(principal: Principal, request: QueryOrderRequest): QueryOrderResponse? { + val order = (if (request.origClientOrderId != null) { + orderRepository.findBySymbolAndClientOrderId(request.symbol, request.origClientOrderId!!) + } else { + orderRepository.findBySymbolAndOrderId(request.symbol, request.orderId!!) + + }).awaitFirstOrNull() ?: return null + + if (order.uuid != principal.name) + throw OpexException(OpexError.Forbidden) + + return order.asQueryResponse(orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull()) + } + + override suspend fun openOrders(principal: Principal, symbol: String?): Flow { + return orderRepository.findByUuidAndSymbolAndStatus( + principal.name, + symbol, + listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) + ).filter { orderModel -> orderModel.constraint != null } + .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } + } + + override suspend fun allOrders(principal: Principal, allOrderRequest: AllOrderRequest): Flow { + return orderRepository.findByUuidAndSymbolAndTimeBetween( + principal.name, + allOrderRequest.symbol, + allOrderRequest.startTime, + allOrderRequest.endTime + ).filter { orderModel -> orderModel.constraint != null } + .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } + } + + override suspend fun allTrades(principal: Principal, request: TradeRequest): Flow { + return tradeRepository.findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan( + principal.name, request.symbol, request.fromTrade, request.startTime, request.endTime + ).map { trade -> + val takerOrder = orderRepository.findByOuid(trade.takerOuid).awaitFirst() + val makerOrder = orderRepository.findByOuid(trade.makerOuid).awaitFirst() + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + TradeResponse( + trade.symbol, + trade.tradeId, + if (trade.takerUuid == principal.name) { + takerOrder.orderId!! + } else { + makerOrder.orderId!! + }, + -1, + if (trade.takerUuid == principal.name) { + trade.takerPrice.toBigDecimal() + } else { + trade.makerPrice.toBigDecimal() + }, + trade.matchedQuantity.toBigDecimal(), + if (isMakerBuyer) { + makerOrder.quoteQuantity!!.toBigDecimal() + } else { + takerOrder.quoteQuantity!!.toBigDecimal() + }, + if (trade.takerUuid == principal.name) { + trade.takerCommision!!.toBigDecimal() + } else { + trade.makerCommision!!.toBigDecimal() + }, + if (trade.takerUuid == principal.name) { + trade.takerCommisionAsset!! + } else { + trade.makerCommisionAsset!! + }, + Date.from( + trade.createDate.atZone(ZoneId.systemDefault()).toInstant() + ), + if (trade.takerUuid == principal.name) { + OrderDirection.ASK == takerOrder.direction + } else { + OrderDirection.ASK == makerOrder.direction + }, + trade.makerUuid == principal.name, + true, + isMakerBuyer + ) + } + } + + + private fun OrderModel.asQueryResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( + symbol, + ouid, + orderId ?: -1, + -1, + clientOrderId ?: "", + price!!.toBigDecimal(), + quantity!!.toBigDecimal(), + orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, + constraint!!.toTimeInForce(), + type!!.toApiOrderType(), + direction!!.toOrderSide(), + null, + null, + Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), + Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), + (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), + quoteQuantity!!.toBigDecimal() + ) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt index b0a44a50b..c3950a26d 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt @@ -1,38 +1,38 @@ -package co.nilin.opex.api.ports.postgres.model - -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import org.springframework.data.annotation.Id -import org.springframework.data.annotation.Version -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table -import java.time.LocalDateTime - -@Table("orders") -class OrderModel( - @Id var id: Long?, - @Column(value = "ouid") - val ouid: String, - val uuid: String, - @Column(value = "client_order_id") - val clientOrderId: String?, - val symbol: String, - @Column(value = "order_id") val orderId: Long?, - @Column("maker_fee") val makerFee: Double?, - @Column("taker_fee") val takerFee: Double?, - @Column("left_side_fraction") val leftSideFraction: Double?, - @Column("right_side_fraction") val rightSideFraction: Double?, - @Column("user_level") val userLevel: String?, - @Column("side") val direction: OrderDirection?, - @Column("match_constraint") val constraint: MatchConstraint?, - @Column("order_type") val type: OrderType?, - @Column("price") val price: Double?, - @Column("quantity") val quantity: Double?, - @Column("quote_quantity") val quoteQuantity: Double?, - @Column("create_date") val createDate: LocalDateTime?, - @Column("update_date") val updateDate: LocalDateTime, - @Version - @Column("version") - var version: Long? = null +package co.nilin.opex.api.ports.postgres.model + +import co.nilin.opex.api.core.inout.MatchConstraint +import co.nilin.opex.api.core.inout.MatchingOrderType +import co.nilin.opex.api.core.inout.OrderDirection +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDateTime + +@Table("orders") +class OrderModel( + @Id var id: Long?, + @Column(value = "ouid") + val ouid: String, + val uuid: String, + @Column(value = "client_order_id") + val clientOrderId: String?, + val symbol: String, + @Column(value = "order_id") val orderId: Long?, + @Column("maker_fee") val makerFee: Double?, + @Column("taker_fee") val takerFee: Double?, + @Column("left_side_fraction") val leftSideFraction: Double?, + @Column("right_side_fraction") val rightSideFraction: Double?, + @Column("user_level") val userLevel: String?, + @Column("side") val direction: OrderDirection?, + @Column("match_constraint") val constraint: MatchConstraint?, + @Column("order_type") val type: MatchingOrderType?, + @Column("price") val price: Double?, + @Column("quantity") val quantity: Double?, + @Column("quote_quantity") val quoteQuantity: Double?, + @Column("create_date") val createDate: LocalDateTime?, + @Column("update_date") val updateDate: LocalDateTime, + @Version + @Column("version") + var version: Long? = null ) \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt index 5897724ee..e72ed124a 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt @@ -2,11 +2,12 @@ package co.nilin.opex.api.ports.postgres.model import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Column import org.springframework.data.relational.core.mapping.Table @Table("symbol_maps") class SymbolMapModel( - @Id val symbol: String, - @Column("value") val value: String, -) \ No newline at end of file + @Id var id: Long?, + val symbol: String, + val aliasKey: String, + val alias: String, +) diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt index dc02cea43..1b38babd4 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt @@ -1,45 +1,39 @@ -package co.nilin.opex.api.ports.postgres.util - -import co.nilin.opex.api.core.inout.OrderSide -import co.nilin.opex.api.core.inout.OrderStatus -import co.nilin.opex.api.core.inout.TimeInForce -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType - -fun MatchConstraint.toTimeInForce(): TimeInForce { - if (this == MatchConstraint.FOK_BUDGET) - return TimeInForce.FOK - if (this == MatchConstraint.IOC_BUDGET) - return TimeInForce.IOC - return TimeInForce.valueOf(this.name) -} - - -fun TimeInForce.toMatchConstraint(): MatchConstraint { - return MatchConstraint.valueOf(this.name) -} - -fun OrderType.toApiOrderType(): co.nilin.opex.api.core.inout.OrderType { - if (this == OrderType.LIMIT_ORDER) - return co.nilin.opex.api.core.inout.OrderType.LIMIT - if (this == OrderType.MARKET_ORDER) - return co.nilin.opex.api.core.inout.OrderType.MARKET - throw IllegalArgumentException("OrderType $this is not supported!") -} - -fun OrderDirection.toOrderSide(): OrderSide { - if (this == OrderDirection.BID) - return OrderSide.BUY - return OrderSide.SELL -} - -fun OrderStatus.isWorking(): Boolean { - return listOf(OrderStatus.NEW, OrderStatus.PARTIALLY_FILLED).contains(this) -} - -fun Int.toOrderStatus(): OrderStatus { - val status = co.nilin.opex.accountant.core.inout.OrderStatus.values() - .find { s -> s.code == this } - return OrderStatus.valueOf(status!!.name) +package co.nilin.opex.api.ports.postgres.util + +import co.nilin.opex.api.core.inout.* + +fun MatchConstraint.toTimeInForce(): TimeInForce { + if (this == MatchConstraint.FOK_BUDGET) + return TimeInForce.FOK + if (this == MatchConstraint.IOC_BUDGET) + return TimeInForce.IOC + return TimeInForce.valueOf(this.name) +} + + +fun TimeInForce.toMatchConstraint(): MatchConstraint { + return MatchConstraint.valueOf(this.name) +} + +fun MatchingOrderType.toApiOrderType(): OrderType { + if (this == MatchingOrderType.LIMIT_ORDER) + return OrderType.LIMIT + if (this == MatchingOrderType.MARKET_ORDER) + return OrderType.MARKET + throw IllegalArgumentException("OrderType $this is not supported!") +} + +fun OrderDirection.toOrderSide(): OrderSide { + if (this == OrderDirection.BID) + return OrderSide.BUY + return OrderSide.SELL +} + +fun OrderStatus.isWorking(): Boolean { + return listOf(OrderStatus.NEW, OrderStatus.PARTIALLY_FILLED).contains(this) +} + +fun Int.toOrderStatus(): OrderStatus { + val status = OrderStatus.values().find { s -> s.code == this } + return OrderStatus.valueOf(status!!.name) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/resources/data.sql b/api/api-ports/api-persister-postgres/src/main/resources/data.sql deleted file mode 100644 index 849c97bd3..000000000 --- a/api/api-ports/api-persister-postgres/src/main/resources/data.sql +++ /dev/null @@ -1,16 +0,0 @@ -INSERT INTO symbol_maps(symbol, value) -VALUES ('btc_usdt', 'BTCUSDT'), - ('eth_usdt', 'ETHUSDT'), - ('eth_btc', 'ETHBTC'), - ('nln_usdt', 'NLNUSDT'), - ('nln_btc', 'NLNBTC') -ON CONFLICT DO NOTHING; - --- Test symbol mapper -INSERT INTO symbol_maps(symbol, value) -VALUES ('tbtc_tusdt', 'TBTCTUSDT'), - ('teth_tusdt', 'TETHTUSDT'), - ('teth_tbtc', 'TETHTBTC'), - ('nln_tusdt', 'NLNTUSDT'), - ('nln_tbtc', 'NLNTBTC') -ON CONFLICT DO NOTHING; diff --git a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql index 497ccb398..4c59f00ce 100644 --- a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql +++ b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql @@ -56,8 +56,11 @@ CREATE TABLE IF NOT EXISTS trades CREATE TABLE IF NOT EXISTS symbol_maps ( - symbol VARCHAR(72) PRIMARY KEY, - value VARCHAR(72) UNIQUE NOT NULL + id SERIAL PRIMARY KEY, + symbol VARCHAR(72) NOT NULL, + alias_key VARCHAR(72) NOT NULL, + alias VARCHAR(72) NOT NULL, + UNIQUE (symbol, alias_key, alias) ); CREATE OR REPLACE FUNCTION interval_generator( diff --git a/api/pom.xml b/api/pom.xml index baf80f4cb..be0a60d25 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -32,16 +32,6 @@ - - co.nilin.opex.matching.engine.core - matching-engine-core - ${project.version} - - - co.nilin.opex.accountant.core - accountant-core - ${project.version} - co.nilin.opex.api.core api-core @@ -77,6 +67,11 @@ interceptors ${project.version} + + co.nilin.opex.utility.preferences + preferences + ${project.version} + org.springframework.cloud spring-cloud-dependencies diff --git a/bc-gateway/bc-gateway-app/Dockerfile b/bc-gateway/bc-gateway-app/Dockerfile index 7c71f9447..268392261 100644 --- a/bc-gateway/bc-gateway-app/Dockerfile +++ b/bc-gateway/bc-gateway-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/bc-gateway/bc-gateway-app/pom.xml b/bc-gateway/bc-gateway-app/pom.xml index c8a5fa93b..7c66de544 100644 --- a/bc-gateway/bc-gateway-app/pom.xml +++ b/bc-gateway/bc-gateway-app/pom.xml @@ -81,6 +81,10 @@ co.nilin.opex.bcgateway.ports.kafka.listener bc-gateway-eventlistener-kafka + + co.nilin.opex.utility.preferences + preferences + io.springfox springfox-boot-starter diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt new file mode 100644 index 000000000..4f4712a2a --- /dev/null +++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/InitializeService.kt @@ -0,0 +1,97 @@ +package co.nilin.opex.bcgateway.app.config + +import co.nilin.opex.bcgateway.ports.postgres.dao.* +import co.nilin.opex.bcgateway.ports.postgres.model.* +import co.nilin.opex.utility.preferences.AddressType +import co.nilin.opex.utility.preferences.Chain +import co.nilin.opex.utility.preferences.Currency +import co.nilin.opex.utility.preferences.Preferences +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.DependsOn +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import javax.annotation.PostConstruct + +@Component +@DependsOn("postgresConfig") +class InitializeService( + private val addressTypeRepository: AddressTypeRepository, + private val chainRepository: ChainRepository, + private val chainAddressTypeRepository: ChainAddressTypeRepository, + private val chainEndpointRepository: ChainEndpointRepository, + private val currencyRepository: CurrencyRepository, + private val currencyImplementationRepository: CurrencyImplementationRepository, + private val chainSyncScheduleRepository: ChainSyncScheduleRepository, + private val walletSyncScheduleRepository: WalletSyncScheduleRepository +) { + @Autowired + private lateinit var preferences: Preferences + + @PostConstruct + fun init() = runBlocking { + addAddressTypes(preferences.addressTypes) + addChains(preferences.chains) + addCurrencies(preferences.currencies) + addSchedules(preferences) + } + + private suspend fun addAddressTypes(data: List) = coroutineScope { + val items = data.mapIndexed { i, it -> + if (addressTypeRepository.existsById(i + 1L).awaitSingle()) null + else AddressTypeModel(null, it.addressType, it.addressRegex, null) + }.filterNotNull() + runCatching { addressTypeRepository.saveAll(items).collectList().awaitSingleOrNull() } + } + + private suspend fun addChains(data: List) = coroutineScope { + data.map { chainRepository.insert(it.name).awaitSingleOrNull() } + val items1 = data.map { + val addressTypeId = addressTypeRepository.findByType(it.addressType).awaitSingle().id!! + ChainAddressTypeModel(null, it.name, addressTypeId) + } + runCatching { chainAddressTypeRepository.saveAll(items1).collectList().awaitSingleOrNull() } + val items2 = data.map { ChainEndpointModel(null, it.name, it.endpointUrl, null, null) } + runCatching { chainEndpointRepository.saveAll(items2).collectList().awaitSingleOrNull() } + } + + private suspend fun addCurrencies(data: List) = coroutineScope { + coroutineScope { + data.forEach { + currencyRepository.insert(it.name, it.symbol).awaitSingleOrNull() + } + } + val items = data.flatMap { it.implementations.map { impl -> it to impl } }.map { (currency, impl) -> + CurrencyImplementationModel( + null, + currency.symbol, + impl.chain, + impl.token, + impl.tokenAddress, + impl.tokenName, + impl.withdrawEnabled, + impl.withdrawFee, + impl.withdrawMin, + impl.decimal + ) + } + runCatching { currencyImplementationRepository.saveAll(items).collectList().awaitSingleOrNull() } + } + + private suspend fun addSchedules(data: Preferences) = coroutineScope { + data.chains.map { + chainSyncScheduleRepository.insert(it.name, it.schedule.delay.toInt(), it.schedule.errorDelay.toInt()) + .awaitSingleOrNull() + } + if (walletSyncScheduleRepository.existsById(1).awaitSingle()) null + else { + val item = WalletSyncScheduleModel( + null, LocalDateTime.now(), data.wallet.schedule.delay, data.wallet.schedule.batchSize + ) + runCatching { walletSyncScheduleRepository.save(item).awaitSingleOrNull() } + } + } +} diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/config/PostgresConfig.kt b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/config/PostgresConfig.kt index 0b6367666..c1dae05d6 100644 --- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/config/PostgresConfig.kt +++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/kotlin/co/nilin/opex/bcgateway/ports/postgres/config/PostgresConfig.kt @@ -10,17 +10,13 @@ import org.springframework.r2dbc.core.DatabaseClient @EnableR2dbcRepositories(basePackages = ["co.nilin.opex"]) class PostgresConfig( db: DatabaseClient, - @Value("classpath:schema.sql") private val schemaResource: Resource, - @Value("classpath:data.sql") private val dataResource: Resource? + @Value("classpath:schema.sql") private val schemaResource: Resource ) { init { val schemaReader = schemaResource.inputStream.reader() val schema = schemaReader.readText().trim() schemaReader.close() - val dataReader = dataResource?.inputStream?.reader() - val data = dataReader?.readText()?.trim() ?: "" - dataReader?.close() - val initDb = db.sql { schema.plus(data) } + val initDb = db.sql { schema } initDb // initialize the database .then() .subscribe() // execute diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/data.sql b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/data.sql deleted file mode 100644 index 1f9282a7d..000000000 --- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/data.sql +++ /dev/null @@ -1,111 +0,0 @@ -INSERT INTO currency -VALUES ('BTC', 'Bitcoin'), - ('ETH', 'Ethereum'), - ('USDT', 'Tether') -ON CONFLICT DO NOTHING; - --- Test currency -INSERT INTO currency -VALUES ('TBTC', 'Bitcoin (Test)'), - ('TETH', 'Ethereum (Test)'), - ('TUSDT', 'Tether (Test)') -ON CONFLICT DO NOTHING; - -INSERT INTO chains -VALUES ('bitcoin'), - ('ethereum'), - ('bsc') -ON CONFLICT DO NOTHING; - --- Test chains -INSERT INTO chains -VALUES ('test-bitcoin'), - ('test-ethereum'), - ('test-bsc') -ON CONFLICT DO NOTHING; - -INSERT INTO address_types(id, address_type, address_regex) -VALUES (1, 'bitcoin', '.*'), - (2, 'ethereum', '.*'), - (3, 'test-bitcoin', '.*') -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('address_types', 'id'), (SELECT MAX(id) FROM address_types)); - -INSERT INTO chain_address_types(chain_name, addr_type_id) -VALUES ('bitcoin', 1), - ('ethereum', 2), - ('bsc', 2) -ON CONFLICT DO NOTHING; - --- Test chain address types -INSERT INTO chain_address_types(chain_name, addr_type_id) -VALUES ('test-bitcoin', 3), - ('test-ethereum', 2), - ('test-bsc', 2) -ON CONFLICT DO NOTHING; - -INSERT INTO currency_implementations(id, - symbol, - chain, - token, - token_address, - token_name, - withdraw_enabled, - withdraw_fee, - withdraw_min, - decimal) -VALUES (1, 'BTC', 'bitcoin', false, null, null, true, 0.0001, 0.0001, 0), - (2, 'ETH', 'ethereum', false, null, null, true, 0.00001, 0.000001, 18), - (3, 'USDT', 'ethereum', true, '0xdac17f958d2ee523a2206206994597c13d831ec7', 'USDT', true, 0.01, 0.01, 6) -ON CONFLICT DO NOTHING; - --- Test currency implementation -INSERT INTO currency_implementations(id, - symbol, - chain, - token, - token_address, - token_name, - withdraw_enabled, - withdraw_fee, - withdraw_min, - decimal) -VALUES (4, 'TBTC', 'test-bitcoin', false, null, null, true, 0.0001, 0.0001, 0), - (5, 'TETH', 'test-ethereum', false, null, null, true, 0.00001, 0.000001, 18), - (6, 'TUSDT', 'test-ethereum', true, '0x110a13fc3efe6a245b50102d2d79b3e76125ae83', 'TUSDT', true, 0.01, 0.01, 6) -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('currency_implementations', 'id'), (SELECT MAX(id) FROM currency_implementations)); - -INSERT INTO chain_endpoints(id, chain_name, endpoint_url) -VALUES (1, 'bitcoin', 'lb://chain-scan-gateway/bitcoin/transfers'), - (2, 'ethereum', 'lb://chain-scan-gateway/eth/transfers'), - (3, 'bsc', 'lb://chain-scan-gateway/bsc/transfers') -ON CONFLICT DO NOTHING; - --- Test chain endpoints -INSERT INTO chain_endpoints(id, chain_name, endpoint_url) -VALUES (4, 'test-bitcoin', 'lb://chain-scan-gateway/test-bitcoin/transfers'), - (5, 'test-ethereum', 'lb://chain-scan-gateway/test-eth/transfers'), - (6, 'test-bsc', 'lb://chain-scan-gateway/test-bsc/transfers') -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('chain_endpoints', 'id'), (SELECT MAX(id) FROM chain_endpoints)); - -INSERT INTO chain_sync_schedules -VALUES ('bitcoin', CURRENT_DATE, 600, 60), - ('ethereum', CURRENT_DATE, 90, 60), - ('bsc', CURRENT_DATE, 90, 60) -ON CONFLICT DO NOTHING; - --- Test chain scan schedules -INSERT INTO chain_sync_schedules -VALUES ('test-bitcoin', CURRENT_DATE, 600, 60), - ('test-ethereum', CURRENT_DATE, 90, 60), - ('test-bsc', CURRENT_DATE, 90, 60) -ON CONFLICT DO NOTHING; - -INSERT INTO wallet_sync_schedules -VALUES (1, CURRENT_DATE, 10, 10000) -ON CONFLICT DO NOTHING; diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql index 8eca3d0e5..6733d6103 100644 --- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql +++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/src/main/resources/schema.sql @@ -40,7 +40,7 @@ CREATE TABLE IF NOT EXISTS assigned_address_chains CREATE TABLE IF NOT EXISTS chain_address_types ( id SERIAL PRIMARY KEY, - chain_name VARCHAR(72) NOT NULL REFERENCES chains (name), + chain_name VARCHAR(72) UNIQUE NOT NULL REFERENCES chains (name), addr_type_id INTEGER NOT NULL REFERENCES address_types (id) ); @@ -122,7 +122,7 @@ CREATE TABLE IF NOT EXISTS currency CREATE TABLE IF NOT EXISTS currency_implementations ( id SERIAL PRIMARY KEY, - symbol VARCHAR(72) NOT NULL, + symbol VARCHAR(72) NOT NULL REFERENCES currency (symbol), chain VARCHAR(72) NOT NULL REFERENCES chains (name), token BOOLEAN NOT NULL, token_address VARCHAR(72), @@ -130,5 +130,6 @@ CREATE TABLE IF NOT EXISTS currency_implementations withdraw_enabled BOOLEAN NOT NULL, withdraw_fee DECIMAL NOT NULL, withdraw_min DECIMAL NOT NULL, - decimal INTEGER NOT NULL + decimal INTEGER NOT NULL, + UNIQUE (symbol, chain) ); diff --git a/bc-gateway/pom.xml b/bc-gateway/pom.xml index 1df44f1cd..6fc01550a 100644 --- a/bc-gateway/pom.xml +++ b/bc-gateway/pom.xml @@ -2,7 +2,7 @@ - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -72,6 +72,11 @@ interceptors ${project.version} + + co.nilin.opex.utility.preferences + preferences + ${project.version} + diff --git a/captcha/captcha-app/Dockerfile b/captcha/captcha-app/Dockerfile index 6053984ee..268392261 100644 --- a/captcha/captcha-app/Dockerfile +++ b/captcha/captcha-app/Dockerfile @@ -2,3 +2,4 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/captcha/captcha-app/src/main/kotlin/co/nilin/opex/captcha/app/controller/Controller.kt b/captcha/captcha-app/src/main/kotlin/co/nilin/opex/captcha/app/controller/Controller.kt index a952fd10f..c5e54803e 100644 --- a/captcha/captcha-app/src/main/kotlin/co/nilin/opex/captcha/app/controller/Controller.kt +++ b/captcha/captcha-app/src/main/kotlin/co/nilin/opex/captcha/app/controller/Controller.kt @@ -14,6 +14,7 @@ import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.util.* +import org.springframework.http.server.reactive.ServerHttpRequest @RestController class Controller( @@ -24,14 +25,12 @@ class Controller( ) @ApiResponses(ApiResponse(message = "OK", code = 200)) @PostMapping("/session", produces = [MediaType.IMAGE_JPEG_VALUE]) - suspend fun getCaptchaImage( - @RequestHeader("X-Forwarded-For", defaultValue = "0.0.0.0") xForwardedFor: List - ): ResponseEntity { + suspend fun getCaptchaImage(request: ServerHttpRequest): ResponseEntity { fun idGen(id: String = UUID.randomUUID().toString().sha256()): String = if (sessionStore.verify(id)) idGen() else id val (answer, image) = captchaGenerator.generate() val id = idGen() - val proof = "$id-$answer-${xForwardedFor.first()}".sha256() + val proof = "$id-$answer-${request.remoteAddress?.address?.hostAddress}".sha256() return ResponseEntity(image, HttpHeaders().apply { set("Captcha-Session-Key", id) set("Captcha-Expire-Timestamp", (sessionStore.put(proof) / 1000).toString()) diff --git a/captcha/captcha-app/src/main/resources/application.yml b/captcha/captcha-app/src/main/resources/application.yml index 4ce73b674..67129ca4e 100644 --- a/captcha/captcha-app/src/main/resources/application.yml +++ b/captcha/captcha-app/src/main/resources/application.yml @@ -1,4 +1,6 @@ -server.port: 8080 +server: + forward-headers-strategy: NATIVE + port: 8080 logging: level: co.nilin: DEBUG diff --git a/captcha/pom.xml b/captcha/pom.xml index a187205f3..debd08cae 100644 --- a/captcha/pom.xml +++ b/captcha/pom.xml @@ -5,7 +5,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/dev.Jenkinsfile b/dev.Jenkinsfile deleted file mode 100644 index 0174f014e..000000000 --- a/dev.Jenkinsfile +++ /dev/null @@ -1,72 +0,0 @@ -pipeline { - agent any - - stages('Deploy') { - stage('Build') { - steps { - setBuildStatus("?", "PENDING") - withMaven( - maven: 'maven-3.6.3' - ) { - sh 'mvn -T 1C -B clean install' - } - } - } - stage('Deliver') { - environment { - DATA = '/var/opex/dev-core' - PANEL_PASS = credentials("v-panel-secret-dev") - BACKEND_USER = credentials("v-backend-secret-dev") - SMTP_PASS = credentials("smtp-secret-dev") - DB_USER = 'opex' - DB_PASS = credentials("db-secret-dev") - DB_BACKUP_USER = 'opex_backup' - DB_BACKUP_PASS = credentials("db-backup-secret-dev") - KEYCLOAK_ADMIN_URL = 'https://demo.opex.dev:8443/auth' - KEYCLOAK_FRONTEND_URL = 'https://demo.opex.dev:8443/auth' - KEYCLOAK_ADMIN_USERNAME = credentials("keycloak-admin-username-dev") - KEYCLOAK_ADMIN_PASSWORD = credentials("keycloak-admin-password-dev") - OPEX_ADMIN_KEYCLOAK_CLIENT_SECRET = credentials("opex-admin-keycloak-client-secret-dev") - VANDAR_API_KEY = credentials("vandar-api-key-dev") - COMPOSE_PROJECT_NAME = 'dev-core' - DEFAULT_NETWORK_NAME = 'dev-opex' - } - steps { - sh 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build --remove-orphans' - sh 'docker image prune -f' - sh 'docker network prune -f' - } - } - } - - post { - always { - echo 'One way or another, I have finished' - } - success { - echo ':)' - setBuildStatus(":)", "SUCCESS") - } - unstable { - echo ':/' - setBuildStatus(":/", "UNSTABLE") - } - failure { - echo ':(' - setBuildStatus(":(", "FAILURE") - } - changed { - echo 'Things were different before...' - } - } -} - -void setBuildStatus(String message, String state) { - step([ - $class : "GitHubCommitStatusSetter", - reposSource : [$class: "ManuallyEnteredRepositorySource", url: "https://github.com/opexdev/OPEX-Core"], - contextSource : [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/build-status"], - errorHandlers : [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]], - statusResultSource: [$class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]]] - ]) -} diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 000000000..fd0601794 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,47 @@ +version: '3.8' +services: + vault: + image: ghcr.io/opexdev/vault-opex:$TAG + build: docker-images/vault + postgres-opex: + image: ghcr.io/opexdev/postgres-opex:$TAG + build: docker-images/postgres + accountant: + image: ghcr.io/opexdev/accountant:$TAG + build: accountant/accountant-app + eventlog: + image: ghcr.io/opexdev/eventlog:$TAG + build: eventlog/eventlog-app + matching-engine: + image: ghcr.io/opexdev/matching-engine:$TAG + build: matching-engine/matching-engine-app + matching-gateway: + image: ghcr.io/opexdev/matching-gateway:$TAG + build: matching-gateway/matching-gateway-app + auth: + image: ghcr.io/opexdev/auth:$TAG + build: user-management/keycloak-gateway + wallet: + image: ghcr.io/opexdev/wallet:$TAG + build: wallet/wallet-app + api: + image: ghcr.io/opexdev/api:$TAG + build: api/api-app + websocket: + image: ghcr.io/opexdev/websocket:$TAG + build: websocket/websocket-app + bc-gateway: + image: ghcr.io/opexdev/bc-gateway:$TAG + build: bc-gateway/bc-gateway-app + storage: + image: ghcr.io/opexdev/storage:$TAG + build: storage/storage-app + referral: + image: ghcr.io/opexdev/referral:$TAG + build: referral/referral-app + captcha: + image: ghcr.io/opexdev/captcha:$TAG + build: captcha/captcha-app + admin: + image: ghcr.io/opexdev/admin:$TAG + build: admin/admin-app diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 062d9d812..000000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.8' -services: - vault-ui: - ports: - - "8001:8000" - consul: - ports: - - "8501:8500" - - "8301:8300" - - "8601:8600" - postgres-accountant: - ports: - - "5442:5432" - postgres-eventlog: - ports: - - "5443:5432" - postgres-auth: - ports: - - "5444:5432" - postgres-wallet: - ports: - - "5445:5432" - postgres-api: - ports: - - "5446:5432" - postgres-bc-gateway: - ports: - - "5447:5432" - postgres-referral: - ports: - - "5448:5432" diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 5ff138293..9ec4ad131 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,31 +1,42 @@ version: '3.8' services: - vault-ui: - ports: - - "8000:8000" - consul: - ports: - - "8500:8500" - - "8300:8300" - - "8600:8600" - postgres-accountant: - ports: - - "5432:5432" - postgres-eventlog: - ports: - - "5433:5432" - postgres-auth: - ports: - - "5434:5432" - postgres-wallet: - ports: - - "5435:5432" - postgres-api: - ports: - - "5436:5432" - postgres-bc-gateway: - ports: - - "5437:5432" - postgres-referral: - ports: - - "5438:5432" + vault: + build: docker-images/vault + postgres-opex: + build: docker-images/postgres + accountant: + build: accountant/accountant-app + volumes: + - "./preferences-$PREFERENCES_IDENTIFIER.yml:/preferences.yml" + eventlog: + build: eventlog/eventlog-app + matching-engine: + build: matching-engine/matching-engine-app + volumes: + - "./preferences-$PREFERENCES_IDENTIFIER.yml:/preferences.yml" + matching-gateway: + build: matching-gateway/matching-gateway-app + auth: + build: user-management/keycloak-gateway + wallet: + build: wallet/wallet-app + volumes: + - "./preferences-$PREFERENCES_IDENTIFIER.yml:/preferences.yml" + api: + build: api/api-app + volumes: + - "./preferences-$PREFERENCES_IDENTIFIER.yml:/preferences.yml" + websocket: + build: websocket/websocket-app + bc-gateway: + build: bc-gateway/bc-gateway-app + volumes: + - "./preferences-$PREFERENCES_IDENTIFIER.yml:/preferences.yml" + storage: + build: storage/storage-app + referral: + build: referral/referral-app + captcha: + build: captcha/captcha-app + admin: + build: admin/admin-app diff --git a/docker-compose.yml b/docker-compose.yml index 000e873c2..17ad65393 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ version: '3.8' services: zookeeper: - image: confluentinc/cp-zookeeper:latest + image: confluentinc/cp-zookeeper:7.1.1 hostname: zookeeper volumes: - - $DATA/zookeeper-data/data:/var/lib/zookeeper/data - - $DATA/zookeeper-data/tx-logs:/var/lib/zookeeper/log + - zookeeper-data:/var/lib/zookeeper/data + - zookeeper-log:/var/lib/zookeeper/log environment: - ALLOW_ANONYMOUS_LOGIN=yes - ZOOKEEPER_CLIENT_PORT=2181 @@ -15,10 +15,10 @@ services: restart_policy: condition: on-failure kafka-1: - image: confluentinc/cp-kafka:latest + image: confluentinc/cp-kafka:7.1.1 hostname: kafka-1 volumes: - - $DATA/kafka-data/kafka-1:/var/lib/kafka/data + - kafka-1:/var/lib/kafka/data environment: - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 - ALLOW_PLAINTEXT_LISTENER=yes @@ -36,10 +36,10 @@ services: restart_policy: condition: on-failure kafka-2: - image: confluentinc/cp-kafka:latest + image: confluentinc/cp-kafka:7.1.1 hostname: kafka-2 volumes: - - $DATA/kafka-data/kafka-2:/var/lib/kafka/data + - kafka-2:/var/lib/kafka/data environment: - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 - ALLOW_PLAINTEXT_LISTENER=yes @@ -57,10 +57,10 @@ services: restart_policy: condition: on-failure kafka-3: - image: confluentinc/cp-kafka:latest + image: confluentinc/cp-kafka:7.1.1 hostname: kafka-3 volumes: - - $DATA/kafka-data/kafka-3:/var/lib/kafka/data + - kafka-3:/var/lib/kafka/data environment: - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 - ALLOW_PLAINTEXT_LISTENER=yes @@ -78,7 +78,7 @@ services: restart_policy: condition: on-failure akhq: - image: tchiotludo/akhq + image: tchiotludo/akhq:0.20.0 environment: AKHQ_CONFIGURATION: | akhq: @@ -92,13 +92,15 @@ services: - kafka-1 - kafka-2 - kafka-3 + deploy: + restart_policy: + condition: on-failure vault: - image: vault + image: ghcr.io/opexdev/vault-opex volumes: - - $DATA/vault:/vault/file:rw - - ./resources/vault:/vault/config:rw + - vault-data:/vault/file environment: - - VAULT_ADDRESS=http://0.0.0.0:8200 + - VAULT_ADDR=http://0.0.0.0:8200 - PANEL_PASS=${PANEL_PASS} - BACKEND_USER=${BACKEND_USER} - SMTP_PASS=${SMTP_PASS} @@ -110,11 +112,11 @@ services: - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD:-hiopex} - OPEX_ADMIN_KEYCLOAK_CLIENT_SECRET=${OPEX_ADMIN_KEYCLOAK_CLIENT_SECRET} - VANDAR_API_KEY=$VANDAR_API_KEY - healthcheck: - retries: 5 cap_add: - IPC_LOCK - entrypoint: /vault/config/workflow-vault.sh + deploy: + restart_policy: + condition: on-failure vault-ui: image: djenriquez/vault-ui environment: @@ -122,6 +124,9 @@ services: - VAULT_AUTH_DEFAULT=USERNAMEPASSWORD depends_on: - vault + deploy: + restart_policy: + condition: on-failure consul: image: consul environment: @@ -132,11 +137,10 @@ services: restart_policy: condition: on-failure redis: - image: redis:alpine + image: redis:7-alpine command: redis-server volumes: - - $DATA/redis-data:/var/lib/redis - - $DATA/redis.conf:/usr/local/etc/redis/redis.conf + - redis-data:/var/lib/redis environment: - REDIS_REPLICATION_MODE=master networks: @@ -145,7 +149,7 @@ services: restart_policy: condition: on-failure postgres-accountant: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -153,12 +157,14 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/accountant-data:/var/lib/postgresql/data/ + - accountant-data:/var/lib/postgresql/data/ networks: - default + deploy: + restart_policy: + condition: on-failure postgres-eventlog: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -166,12 +172,14 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/eventlog-data:/var/lib/postgresql/data/ + - eventlog-data:/var/lib/postgresql/data/ networks: - default + deploy: + restart_policy: + condition: on-failure postgres-auth: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -179,15 +187,14 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/auth-data:/var/lib/postgresql/data/ + - auth-data:/var/lib/postgresql/data/ networks: - default deploy: restart_policy: condition: on-failure postgres-wallet: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -195,15 +202,14 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/wallet-data:/var/lib/postgresql/data/ + - wallet-data:/var/lib/postgresql/data/ networks: - default deploy: restart_policy: condition: on-failure postgres-api: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -211,15 +217,14 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/api-data:/var/lib/postgresql/data/ + - api-data:/var/lib/postgresql/data/ networks: - default deploy: restart_policy: condition: on-failure postgres-bc-gateway: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -227,15 +232,14 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/bc-gateway-data:/var/lib/postgresql/data/ + - bc-gateway-data:/var/lib/postgresql/data/ networks: - default deploy: restart_policy: condition: on-failure postgres-referral: - image: postgres:14-alpine + image: ghcr.io/opexdev/postgres-opex environment: - POSTGRES_USER=${DB_USER:-opex} - POSTGRES_PASSWORD=${DB_PASS:-hiopex} @@ -243,14 +247,12 @@ services: - POSTGRES_BACKUP_USER=${DB_BACKUP_USER:-opex_backup} - POSTGRES_BACKUP_PASSWORD=${DB_BACKUP_PASS:-hiopex} volumes: - - ./resources/postgres/init-backup-user.sh:/docker-entrypoint-initdb.d/init-backup-user.sh - - $DATA/referral-data:/var/lib/postgresql/data/ + - referral-data:/var/lib/postgresql/data/ deploy: restart_policy: condition: on-failure accountant: - build: - context: accountant/accountant-app + image: ghcr.io/opexdev/accountant environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - SPRING_PROFILES_ACTIVE=scheduled @@ -260,6 +262,9 @@ services: - BACKEND_USER=${BACKEND_USER} - VAULT_HOST=vault - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL + - PREFERENCES=$PREFERENCES + configs: + - preferences.yml networks: - default depends_on: @@ -270,9 +275,11 @@ services: - consul - vault - postgres-accountant + deploy: + restart_policy: + condition: on-failure eventlog: - build: - context: eventlog/eventlog-app + image: ghcr.io/opexdev/eventlog environment: - JAVA_OPTS=-Xmx256m - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 @@ -289,13 +296,18 @@ services: - consul - vault - postgres-eventlog + deploy: + restart_policy: + condition: on-failure matching-engine: - build: - context: matching-engine/matching-engine-app + image: ghcr.io/opexdev/matching-engine environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 - REDIS_HOST=redis + - PREFERENCES=$PREFERENCES + configs: + - preferences.yml networks: - default depends_on: @@ -303,9 +315,11 @@ services: - kafka-2 - kafka-3 - redis + deploy: + restart_policy: + condition: on-failure matching-gateway: - build: - context: matching-gateway/matching-gateway-app + image: ghcr.io/opexdev/matching-gateway environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 @@ -319,16 +333,16 @@ services: - auth - consul - matching-engine + deploy: + restart_policy: + condition: on-failure auth: - build: - context: user-management/keycloak-gateway + image: ghcr.io/opexdev/auth environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 - CONSUL_HOST=consul - DB_IP_PORT=postgres-auth - - PROXY_ADDRESS_FORWARDING=true - - WORKING_DIR=$DATA - BACKEND_USER=$BACKEND_USER - ADMIN_URL=$KEYCLOAK_ADMIN_URL - FRONTEND_URL=$KEYCLOAK_FRONTEND_URL @@ -348,8 +362,7 @@ services: restart_policy: condition: on-failure wallet: - build: - context: wallet/wallet-app + image: ghcr.io/opexdev/wallet environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 @@ -358,6 +371,9 @@ services: - BACKEND_USER=${BACKEND_USER} - VAULT_HOST=vault - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL + - PREFERENCES=$PREFERENCES + configs: + - preferences.yml depends_on: - kafka-1 - kafka-2 @@ -372,8 +388,7 @@ services: restart_policy: condition: on-failure api: - build: - context: api/api-app + image: ghcr.io/opexdev/api environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 @@ -382,6 +397,9 @@ services: - BACKEND_USER=${BACKEND_USER} - VAULT_HOST=vault - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL + - PREFERENCES=$PREFERENCES + configs: + - preferences.yml depends_on: - kafka-1 - kafka-2 @@ -401,8 +419,7 @@ services: restart_policy: condition: on-failure websocket: - build: - context: websocket/websocket-app + image: ghcr.io/opexdev/websocket environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 @@ -424,8 +441,7 @@ services: restart_policy: condition: on-failure bc-gateway: - build: - context: bc-gateway/bc-gateway-app + image: ghcr.io/opexdev/bc-gateway environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - SPRING_PROFILES_DEFAULT=scheduled @@ -435,6 +451,9 @@ services: - BACKEND_USER=${BACKEND_USER} - VAULT_HOST=vault - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL + - PREFERENCES=$PREFERENCES + configs: + - preferences.yml depends_on: - kafka-1 - kafka-2 @@ -450,15 +469,14 @@ services: restart_policy: condition: on-failure storage: - build: - context: storage/storage-app + image: ghcr.io/opexdev/storage environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - CONSUL_HOST=consul - ROOT_DIR=/storage - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL volumes: - - $DATA/storage-data:/storage + - storage-data:/storage depends_on: - auth - consul @@ -468,8 +486,7 @@ services: restart_policy: condition: on-failure referral: - build: - context: referral/referral-app + image: ghcr.io/opexdev/referral environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044 - CONSUL_HOST=consul @@ -491,8 +508,7 @@ services: restart_policy: condition: on-failure captcha: - build: - context: captcha/captcha-app + image: ghcr.io/opexdev/captcha environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044 - CONSUL_HOST=consul @@ -503,8 +519,7 @@ services: restart_policy: condition: on-failure admin: - build: - context: admin/admin-app + image: ghcr.io/opexdev/admin environment: - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 @@ -513,7 +528,7 @@ services: - BACKEND_USER=${BACKEND_USER} - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL volumes: - - $DATA/admin-data:/admin + - admin-data:/admin depends_on: - kafka-1 - kafka-2 @@ -526,7 +541,31 @@ services: deploy: restart_policy: condition: on-failure +volumes: + zookeeper-data: + zookeeper-log: + kafka-1: + kafka-2: + kafka-3: + vault-data: + redis-data: + accountant-data: + eventlog-data: + auth-data: + wallet-data: + api-data: + bc-gateway-data: + referral-data: + storage-data: + admin-data: networks: default: - name: ${DEFAULT_NETWORK_NAME:-opex} driver: bridge +secrets: + opex_dev_crt: + file: opex.dev.crt + private_pem: + file: private.pem +configs: + preferences.yml: + file: preferences.yml diff --git a/docker-images/postgres/Dockerfile b/docker-images/postgres/Dockerfile new file mode 100644 index 000000000..c2d284111 --- /dev/null +++ b/docker-images/postgres/Dockerfile @@ -0,0 +1,4 @@ +FROM postgres:14-alpine +COPY ["add-backup-user.sh", "/docker-entrypoint-initdb.d/"] +EXPOSE 5432 +HEALTHCHECK --interval=15s --start-period=30s --retries=15 CMD pg_isready -U $POSTGRES_USER -d $POSTGRES_DB -q || exit 1 diff --git a/resources/postgres/init-backup-user.sh b/docker-images/postgres/add-backup-user.sh similarity index 100% rename from resources/postgres/init-backup-user.sh rename to docker-images/postgres/add-backup-user.sh diff --git a/docker-images/vault/Dockerfile b/docker-images/vault/Dockerfile new file mode 100644 index 000000000..c252d77bb --- /dev/null +++ b/docker-images/vault/Dockerfile @@ -0,0 +1,5 @@ +FROM vault:1.10.1 +COPY ["backend-policy.hcl", "panel-policy.hcl", "vault.json", "workflow-vault.sh", "/vault/config/"] +EXPOSE 8200 +ENTRYPOINT /vault/config/workflow-vault.sh +HEALTHCHECK --interval=15s --start-period=15s --retries=15 CMD wget -qO- http://localhost:8200/v1/sys/health &>/dev/null || exit 1 diff --git a/resources/vault/backend-policy.hcl b/docker-images/vault/backend-policy.hcl similarity index 100% rename from resources/vault/backend-policy.hcl rename to docker-images/vault/backend-policy.hcl diff --git a/resources/vault/panel-policy.hcl b/docker-images/vault/panel-policy.hcl similarity index 100% rename from resources/vault/panel-policy.hcl rename to docker-images/vault/panel-policy.hcl diff --git a/resources/vault/vault.json b/docker-images/vault/vault.json similarity index 99% rename from resources/vault/vault.json rename to docker-images/vault/vault.json index 105035688..fbe384e20 100644 --- a/resources/vault/vault.json +++ b/docker-images/vault/vault.json @@ -13,4 +13,4 @@ "default_lease_ttl": "168h", "max_lease_ttl": "0h", "api_addr": "http://0.0.0.0:8200" -} \ No newline at end of file +} diff --git a/docker-images/vault/workflow-vault.sh b/docker-images/vault/workflow-vault.sh new file mode 100755 index 000000000..f7ff5ff79 --- /dev/null +++ b/docker-images/vault/workflow-vault.sh @@ -0,0 +1,101 @@ +#!/bin/sh +set -e + +unseal() { + ## Generate keys + if [ ! -f /vault/file/generated_keys.txt ]; then + vault operator init >/vault/file/generated_keys.txt + fi + + ## Parse unsealed keys + (grep "Unseal Key " /vault/file/keys.txt + + while IFS= read -r line; do + vault operator unseal $line + done /vault/file/tokens.txt + while IFS= read -r line; do + export VAULT_TOKEN=${line} + done org.springframework.cloud spring-cloud-starter-vault-config + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/eventlog/pom.xml b/eventlog/pom.xml index afa61efb4..91e9e2c03 100644 --- a/eventlog/pom.xml +++ b/eventlog/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/matching-engine/matching-engine-app/Dockerfile b/matching-engine/matching-engine-app/Dockerfile index 7c71f9447..268392261 100644 --- a/matching-engine/matching-engine-app/Dockerfile +++ b/matching-engine/matching-engine-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/matching-engine/matching-engine-app/pom.xml b/matching-engine/matching-engine-app/pom.xml index 93ea2bde7..c27f3b7b4 100644 --- a/matching-engine/matching-engine-app/pom.xml +++ b/matching-engine/matching-engine-app/pom.xml @@ -43,6 +43,14 @@ co.nilin.opex.matching.engine.ports.redis matching-engine-snapshots-redis + + org.springframework.boot + spring-boot-starter-actuator + + + co.nilin.opex.utility.preferences + preferences + diff --git a/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/AppConfig.kt b/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/AppConfig.kt index ef535cba3..246c936ee 100644 --- a/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/AppConfig.kt +++ b/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/AppConfig.kt @@ -12,16 +12,14 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class AppConfig { - - @Value("\${spring.app.symbols}") - private val symbols: String? = null + @Autowired + private lateinit var symbols: List @Bean @ConditionalOnMissingBean(value = [OrderBookPersister::class]) @@ -38,22 +36,21 @@ class AppConfig { @Autowired fun configureOrderBooks(orderBookPersister: OrderBookPersister) { - symbols!!.split(",") - .forEach { symbol -> - CoroutineScope(AppSchedulers.generalExecutor).launch { - val lastOrderBook = orderBookPersister.loadLastState(symbol) - //todo: load db orders from last order in order book and put in order book - //todo: add missing orders to lastOrderBook or create one - if (lastOrderBook != null) { - withContext(coroutineContext) { - OrderBooks.reloadOrderBook(lastOrderBook) - } - } else { - OrderBooks.createOrderBook(symbol) + symbols.forEach { symbol -> + CoroutineScope(AppSchedulers.generalExecutor).launch { + val lastOrderBook = orderBookPersister.loadLastState(symbol) + //todo: load db orders from last order in order book and put in order book + //todo: add missing orders to lastOrderBook or create one + if (lastOrderBook != null) { + withContext(coroutineContext) { + OrderBooks.reloadOrderBook(lastOrderBook) } - + } else { + OrderBooks.createOrderBook(symbol) } + } + } } @Bean @@ -80,5 +77,4 @@ class AppConfig { fun configureMatchingEngineListener(exchangeEventHandler: ExchangeEventHandler) { exchangeEventHandler.register() } - -} \ No newline at end of file +} diff --git a/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt b/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt new file mode 100644 index 000000000..6fa85727c --- /dev/null +++ b/matching-engine/matching-engine-app/src/main/kotlin/co/nilin/opex/matching/engine/app/config/InitializeService.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.matching.engine.app.config + +import co.nilin.opex.utility.preferences.Preferences +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class InitializeService() { + @Autowired + private lateinit var preferences: Preferences + + @Bean("symbols") + fun getSymbols(): List = preferences.markets.map { it.pair ?: "${it.leftSide}_${it.rightSide}" } +} diff --git a/matching-engine/matching-engine-app/src/main/resources/application.yml b/matching-engine/matching-engine-app/src/main/resources/application.yml index 0b6e0d6cb..694e6c8a0 100644 --- a/matching-engine/matching-engine-app/src/main/resources/application.yml +++ b/matching-engine/matching-engine-app/src/main/resources/application.yml @@ -10,5 +10,3 @@ spring: redis: host: ${REDIS_HOST:localhost} port: 6379 - app: - symbols: btc_usdt,eth_usdt,eth_btc,tbtc_tusdt,teth_tusdt,teth_tbtc diff --git a/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt b/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt index 8f2edb314..2817f2c00 100644 --- a/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-eventlistener-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/listener/config/OrderKafkaConfig.kt @@ -29,8 +29,8 @@ class OrderKafkaConfig { @Value("\${spring.kafka.consumer.group-id}") private lateinit var groupId: String - @Value("\${spring.app.symbols}") - private lateinit var symbols: String + @Autowired + private lateinit var symbols: List @Bean("consumerConfigs") fun consumerConfigs(): Map { @@ -60,7 +60,7 @@ class OrderKafkaConfig { @Qualifier("orderKafkaTemplate") template: KafkaTemplate, @Qualifier("orderConsumerFactory") consumerFactory: ConsumerFactory ) { - val topics = symbols.split(",").map { s -> "orders_$s" }.toTypedArray() + val topics = symbols.map { s -> "orders_$s" }.toTypedArray() val containerProps = ContainerProperties(*topics) containerProps.messageListener = orderKafkaListener val container = KafkaMessageListenerContainer(consumerFactory, containerProps) @@ -91,4 +91,4 @@ class OrderKafkaConfig { return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) } -} \ No newline at end of file +} diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml index 5ec8e2d4b..b2697b5cb 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml @@ -32,6 +32,10 @@ co.nilin.opex.matching.engine.core matching-engine-core + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.kafka spring-kafka diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt index 6184f49aa..61186d837 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt @@ -7,10 +7,10 @@ import org.springframework.context.annotation.Configuration import org.springframework.kafka.core.KafkaAdmin @Configuration -class KafkaAdminConfig { - +class KafkaAdminConfig( @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String + private val bootstrapServers: String +) { @Bean fun admin(): KafkaAdmin? { diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt index 5cbc89854..ee8446e29 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt @@ -3,7 +3,6 @@ package co.nilin.opex.matching.engine.ports.kafka.submitter.config import org.apache.kafka.clients.admin.NewTopic import org.apache.kafka.common.config.TopicConfig import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Configuration import org.springframework.context.support.GenericApplicationContext import org.springframework.kafka.config.TopicBuilder @@ -12,13 +11,12 @@ import java.util.function.Supplier @Configuration class KafkaTopicConfig { - @Value("\${spring.app.symbols}") - private lateinit var symbols: String + @Autowired + private lateinit var symbols: List @Autowired fun createTopics(applicationContext: GenericApplicationContext) { - symbols.split(",") - .map { s -> "orders_$s" } + symbols.map { s -> "orders_$s" } .forEach { topic -> applicationContext.registerBean("topic_${topic}", NewTopic::class.java, Supplier { TopicBuilder.name(topic) @@ -29,8 +27,7 @@ class KafkaTopicConfig { }) } - symbols.split(",") - .map { s -> "events_$s" } + symbols.map { s -> "events_$s" } .forEach { topic -> applicationContext.registerBean("topic_${topic}", NewTopic::class.java, Supplier { TopicBuilder.name(topic) @@ -41,8 +38,7 @@ class KafkaTopicConfig { }) } - symbols.split(",") - .map { s -> "trades_$s" } + symbols.map { s -> "trades_$s" } .forEach { topic -> applicationContext.registerBean("topic_${topic}", NewTopic::class.java, Supplier { TopicBuilder.name(topic) @@ -54,4 +50,4 @@ class KafkaTopicConfig { } } -} \ No newline at end of file +} diff --git a/matching-engine/pom.xml b/matching-engine/pom.xml index 17e8be8dc..c53a2590f 100644 --- a/matching-engine/pom.xml +++ b/matching-engine/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -67,6 +67,11 @@ interceptors ${project.version} + + co.nilin.opex.utility.preferences + preferences + ${project.version} + diff --git a/matching-gateway/matching-gateway-app/Dockerfile b/matching-gateway/matching-gateway-app/Dockerfile index 7c71f9447..268392261 100644 --- a/matching-gateway/matching-gateway-app/Dockerfile +++ b/matching-gateway/matching-gateway-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt index e27767f48..6b91aa20e 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt @@ -1,16 +1,18 @@ -package co.nilin.opex.matching.gateway.app - -import co.nilin.opex.utility.error.EnableOpexErrorHandler -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication -import org.springframework.context.annotation.ComponentScan -import springfox.documentation.swagger2.annotations.EnableSwagger2 - -@SpringBootApplication -@ComponentScan("co.nilin.opex") -@EnableOpexErrorHandler -class MatchingGatewayApp - -fun main(args: Array) { - runApplication(*args) -} +package co.nilin.opex.matching.gateway.app + +import co.nilin.opex.utility.error.EnableOpexErrorHandler +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan +import org.springframework.scheduling.annotation.EnableScheduling +import springfox.documentation.swagger2.annotations.EnableSwagger2 + +@SpringBootApplication +@ComponentScan("co.nilin.opex") +@EnableOpexErrorHandler +@EnableScheduling +class MatchingGatewayApp + +fun main(args: Array) { + runApplication(*args) +} diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt new file mode 100644 index 000000000..ae871751c --- /dev/null +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.matching.gateway.app.inout + +data class BooleanResponse(val result: Boolean) \ No newline at end of file diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt index 1e129e44f..1e76757b1 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt @@ -1,30 +1,30 @@ package co.nilin.opex.matching.gateway.app.proxy import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.matching.gateway.app.inout.BooleanResponse import co.nilin.opex.matching.gateway.app.inout.PairFeeConfig import co.nilin.opex.matching.gateway.app.spi.AccountantApiProxy import kotlinx.coroutines.reactive.awaitFirst import org.springframework.beans.factory.annotation.Value -import org.springframework.core.ParameterizedTypeReference import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono import java.math.BigDecimal -import java.net.URI - -inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {} @Component class AccountantProxyImpl( - @Value("\${app.accountant.url}") val accountantBaseUrl: String, val webClient: WebClient + @Value("\${app.accountant.url}") + val accountantBaseUrl: String, + val webClient: WebClient ) : AccountantApiProxy { + override suspend fun canCreateOrder(uuid: String, symbol: String, value: BigDecimal): Boolean { - data class BooleanResponse(val result: Boolean) return webClient.get() - .uri(URI.create("$accountantBaseUrl/$uuid/create_order/${value}_${symbol}/allowed")) + .uri("$accountantBaseUrl/$uuid/create_order/${value}_${symbol}/allowed") .header("Content-Type", "application/json") .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) + .bodyToMono() .log() .awaitFirst() .result @@ -33,18 +33,16 @@ class AccountantProxyImpl( override suspend fun fetchPairFeeConfig(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { return webClient.get() .uri( - URI.create( - if (userLevel.isBlank()) { - "$accountantBaseUrl/config/${pair}/fee/${direction}" - } else { - "$accountantBaseUrl/config/${pair}/fee/${direction}-${userLevel}" - } - ) + if (userLevel.isBlank()) { + "$accountantBaseUrl/config/${pair}/fee/${direction}" + } else { + "$accountantBaseUrl/config/${pair}/fee/${direction}-${userLevel}" + } ) .header("Content-Type", "application/json") .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) + .bodyToMono() .log() .awaitFirst() } diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt index f78ec858d..3647c7f21 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt @@ -10,26 +10,34 @@ import co.nilin.opex.matching.gateway.app.spi.PairConfigLoader import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitRequest import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitResult import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.EventSubmitter +import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.KafkaHealthIndicator import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.OrderSubmitter import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.throwError +import co.nilin.opex.utility.error.data.OpexException +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import java.util.* @Service class OrderService( val accountantApiProxy: AccountantApiProxy, val orderSubmitter: OrderSubmitter, val eventSubmitter: EventSubmitter, - val pairConfigLoader: PairConfigLoader + val pairConfigLoader: PairConfigLoader, + private val kafkaHealthIndicator: KafkaHealthIndicator, ) { + + private val logger = LoggerFactory.getLogger(OrderService::class.java) + suspend fun submitNewOrder(createOrderRequest: CreateOrderRequest): OrderSubmitResult { val symbolSides = createOrderRequest.pair.split("_") val symbol = if (createOrderRequest.direction == OrderDirection.ASK) symbolSides[0] else symbolSides[1] - if (!accountantApiProxy.canCreateOrder( + val pairFeeConfig = pairConfigLoader.load(createOrderRequest.pair, createOrderRequest.direction, "") + + val canCreateOrder = runCatching { + accountantApiProxy.canCreateOrder( createOrderRequest.uuid!!, symbol, if (createOrderRequest.direction == OrderDirection.ASK) @@ -37,26 +45,22 @@ class OrderService( else createOrderRequest.quantity.multiply(createOrderRequest.price) ) - ) { - throwError(OpexError.SubmitOrderForbiddenByAccountant) - } - val pairFeeConfig = pairConfigLoader.load( - createOrderRequest.pair, createOrderRequest.direction, "" - ) + }.onFailure { logger.error(it.message) }.getOrElse { false } + + if (!canCreateOrder) + throw OpexException(OpexError.SubmitOrderForbiddenByAccountant) + + if (!kafkaHealthIndicator.isHealthy) + throw OpexException(OpexError.ServiceUnavailable) + val orderSubmitRequest = OrderSubmitRequest( - UUID.randomUUID().toString(), - createOrderRequest.uuid!! //get from auth2 - , - null, + createOrderRequest.uuid!!, //get from auth2 Pair(symbolSides[0], symbolSides[1]), - createOrderRequest.price.divide( - pairFeeConfig.pairConfig.rightSideFraction - .toBigDecimal() - ).longValueExact(), - createOrderRequest.quantity.divide( - pairFeeConfig.pairConfig.leftSideFraction - .toBigDecimal() - ) + createOrderRequest.price + .divide(pairFeeConfig.pairConfig.rightSideFraction.toBigDecimal()) + .longValueExact(), + createOrderRequest.quantity + .divide(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) .longValueExact(), createOrderRequest.direction, createOrderRequest.matchConstraint, diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt new file mode 100644 index 000000000..daa67266b --- /dev/null +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.matching.gateway.ports.kafka.submitter.config + +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.clients.admin.AdminClientConfig +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.KafkaAdmin + +@Configuration +class KafkaAdminConfig( + @Value("\${spring.kafka.bootstrap-servers}") + private val bootstrapServers: String +) { + + @Bean + fun admin(): KafkaAdmin { + val configs = hashMapOf(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers) + return KafkaAdmin(configs) + } + + @Bean + fun adminClient(admin: KafkaAdmin): AdminClient { + return AdminClient.create(admin.configurationProperties) + } + +} \ No newline at end of file diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt index 978399f2d..a295e5ba2 100644 --- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt @@ -4,41 +4,16 @@ import co.nilin.opex.matching.engine.core.model.MatchConstraint import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.matching.engine.core.model.OrderType import co.nilin.opex.matching.engine.core.model.Pair +import java.util.* -class OrderSubmitRequest() { - - lateinit var ouid: String - lateinit var uuid: String - var orderId: Long? = null - lateinit var pair: Pair - var price: Long = 0 - var quantity: Long = 0 - var direction: OrderDirection = OrderDirection.BID - var matchConstraint: MatchConstraint = MatchConstraint.GTC - var orderType: OrderType = OrderType.LIMIT_ORDER - - - constructor( - ouid: String, - uuid: String, - orderId: Long?, - pair: Pair, - price: Long, - quantity: Long, - direction: OrderDirection, - matchConstraint: MatchConstraint, - orderType: OrderType - ) : this() { - this.ouid = ouid - this.uuid = uuid - this.orderId = orderId - this.pair = pair - this.price = price - this.quantity = quantity - this.direction = direction - this.matchConstraint = matchConstraint - this.orderType = orderType - } - - -} \ No newline at end of file +data class OrderSubmitRequest( + val uuid: String, + val pair: Pair, + val price: Long, + val quantity: Long, + val direction: OrderDirection, + val matchConstraint: MatchConstraint, + val orderType: OrderType, + val ouid: String = UUID.randomUUID().toString(), + val orderId: Long? = null, +) \ No newline at end of file diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt new file mode 100644 index 000000000..13f497b30 --- /dev/null +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt @@ -0,0 +1,31 @@ +package co.nilin.opex.matching.gateway.ports.kafka.submitter.service + +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.clients.admin.DescribeClusterOptions +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class KafkaHealthIndicator(private val adminClient: AdminClient) { + + private val logger = LoggerFactory.getLogger(KafkaHealthIndicator::class.java) + private val options = DescribeClusterOptions().timeoutMs(1000) + private val healthyNodeSize = 3 + final var isHealthy = true + private set + + @Scheduled(fixedDelay = 5000, initialDelay = 5000) + fun check() { + isHealthy = try { + val description = adminClient.describeCluster(options) + if (description.nodes().get().size < healthyNodeSize) + throw IllegalStateException("Insufficient nodes") + true + } catch (e: Exception) { + logger.warn("Kafka is not healthy!: ${e.message}") + false + } + } + +} \ No newline at end of file diff --git a/matching-gateway/pom.xml b/matching-gateway/pom.xml index 1d20c8c5b..132b257f2 100644 --- a/matching-gateway/pom.xml +++ b/matching-gateway/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index bb5e15ea8..9e524604f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 co.nilin.opex - OPEX-Core + core pom 1.0-SNAPSHOT diff --git a/preferences-demo.yml b/preferences-demo.yml new file mode 100644 index 000000000..3cbd08b42 --- /dev/null +++ b/preferences-demo.yml @@ -0,0 +1,203 @@ +addressTypes: + - addressType: bitcoin + addressRegex: "*" + - addressType: ethereum + addressRegex: "*" +chains: + - name: bitcoin + addressType: bitcoin + endpointUrl: lb://chain-scan-gateway/bitcoin/transfers + schedule: + delay: 600 + errorDelay: 60 + - name: ethereum + addressType: ethereum + endpointUrl: lb://chain-scan-gateway/eth/transfers + schedule: + delay: 90 + errorDelay: 60 + - name: bsc + addressType: ethereum + endpointUrl: lb://chain-scan-gateway/bsc/transfers + schedule: + delay: 90 + errorDelay: 60 +currencies: + - symbol: BTC + name: Bitcoin + precision: 0.000001 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: bitcoin + withdrawFee: 0.0001 + withdrawMin: 0.0001 + decimal: 0 + - symbol: ETH + name: Ethereum + precision: 0.000001 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: ethereum + withdrawFee: 0.00001 + withdrawMin: 0.000001 + decimal: 18 + - symbol: BNB + name: Binance + precision: 0.0001 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: bsc + withdrawFee: 0.00001 + withdrawMin: 0.000001 + decimal: 18 + - symbol: BUSD + name: Binance USD + precision: 0.01 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: bsc + token: true + tokenAddress: 0xe9e7cea3dedca5984780bafc599bd69add087d56 + tokenName: BUSD Token + withdrawFee: 0.01 + withdrawMin: 0.01 + decimal: 18 + - symbol: IRT + name: Toman + precision: 0.1 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 +markets: + - leftSide: BTC + rightSide: BUSD + aliases: + - key: binance + alias: BTCBUSD + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: ETH + rightSide: BUSD + aliases: + - key: binance + alias: ETHBUSD + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: BNB + rightSide: BUSD + aliases: + - key: binance + alias: BNBBUSD + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: BTC + rightSide: IRT + aliases: + - key: binance + alias: BTCIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: ETH + rightSide: IRT + aliases: + - key: binance + alias: ETHIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: BNB + rightSide: IRT + aliases: + - key: binance + alias: BNBIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: BUSD + rightSide: IRT + aliases: + - key: binance + alias: BUSDIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 +wallet: + schedule: + delay: 10 + batchSize: 10000 +userLimits: + - owner: 1 + action: withdraw + walletType: main + withdrawFee: 0.0001 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 +system: + walletTitle: system + walletLevel: basic diff --git a/preferences-dev.yml b/preferences-dev.yml new file mode 100644 index 000000000..7f0d37ea4 --- /dev/null +++ b/preferences-dev.yml @@ -0,0 +1,160 @@ +addressTypes: + - addressType: ethereum + addressRegex: "*" + - addressType: test-bitcoin + addressRegex: "*" +chains: + - name: test-bitcoin + addressType: test-bitcoin + endpointUrl: lb://chain-scan-gateway/test-bitcoin/transfers + schedule: + delay: 600 + errorDelay: 60 + - name: test-ethereum + addressType: ethereum + endpointUrl: lb://chain-scan-gateway/test-eth/transfers + schedule: + delay: 90 + errorDelay: 60 +currencies: + - symbol: IRT + name: Toman + precision: 0.1 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + gift: 100000 + - symbol: TBTC + name: Bitcoin (Test) + precision: 0.000001 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: test-bitcoin + withdrawFee: 0.0001 + withdrawMin: 0.0001 + decimal: 0 + gift: 21000000 + - symbol: TETH + name: Ethereum (Test) + precision: 0.000001 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: test-ethereum + withdrawFee: 0.00001 + withdrawMin: 0.000001 + decimal: 18 + gift: 2000 + - symbol: TUSDT + name: Tether (Test) + precision: 0.01 + mainBalance: 10000 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 + implementations: + - chain: ethereum + token: true + tokenAddress: 0x110a13fc3efe6a245b50102d2d79b3e76125ae83 + tokenName: Tether USD + withdrawFee: 0.01 + withdrawMin: 0.01 + decimal: 6 + gift: 1000000 +markets: + - leftSide: TBTC + rightSide: TUSDT + aliases: + - key: binance + alias: TBTCTUSDT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: TETH + rightSide: TUSDT + aliases: + - key: binance + alias: TETHTUSDT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: TBTC + rightSide: IRT + aliases: + - key: binance + alias: TBTCIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: TETH + rightSide: IRT + aliases: + - key: binance + alias: TETHIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - leftSide: TUSDT + rightSide: IRT + aliases: + - key: binance + alias: TUSDTIRT + feeConfigs: + - direction: ASK + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 + - direction: BID + userLevel: "*" + makerFee: 0.01 + takerFee: 0.01 +wallet: + schedule: + delay: 10 + batchSize: 10000 +userLimits: + - owner: 1 + action: withdraw + walletType: main + withdrawFee: 0.0001 + dailyTotal: 1000 + dailyCount: 100 + monthlyTotal: 30000 + monthlyCount: 3000 +system: + walletTitle: system + walletLevel: basic diff --git a/referral/pom.xml b/referral/pom.xml index d010dd142..b8660a832 100644 --- a/referral/pom.xml +++ b/referral/pom.xml @@ -5,7 +5,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/referral/referral-app/Dockerfile b/referral/referral-app/Dockerfile index 6053984ee..26f866a0c 100644 --- a/referral/referral-app/Dockerfile +++ b/referral/referral-app/Dockerfile @@ -2,3 +2,4 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 diff --git a/referral/referral-app/src/main/resources/application.yml b/referral/referral-app/src/main/resources/application.yml index 1928f4875..7c7ee17e9 100644 --- a/referral/referral-app/src/main/resources/application.yml +++ b/referral/referral-app/src/main/resources/application.yml @@ -46,6 +46,7 @@ spring: import: vault://secret/${spring.application.name} swagger.authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token app: + address: 1 wallet: url: lb://opex-wallet/ api: diff --git a/resources/vault/workflow-vault.sh b/resources/vault/workflow-vault.sh deleted file mode 100755 index 51d8d14e2..000000000 --- a/resources/vault/workflow-vault.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/sh -vault server -config /vault/config/vault.json & - -## Export values -export VAULT_ADDR='http://0.0.0.0:8200' -export VAULT_SKIP_VERIFY='true' - -# -sleep 10 - -if [ ! -f /vault/file/generated_keys.txt ]; then - echo "Vault init" - vault operator init > /vault/file/generated_keys.txt -fi -echo "Generated Keys:" -cat /vault/file/generated_keys.txt -## Parse unsealed keys -(grep "Unseal Key " < /vault/file/generated_keys.txt | cut -c15-) > /vault/file/keys.txt - -echo "Keys:" -cat /vault/file/keys.txt - -while IFS= read -r line; do - echo "Key read from file: $line" - vault operator unseal $line -done < /vault/file/keys.txt -# -## Get root token -(grep "Initial Root Token: " < /vault/file/generated_keys.txt | cut -c21-) > /vault/file/tokens.txt -while IFS= read -r line; do - echo "Root token read from file: $line" - export VAULT_TOKEN=${line} -done < /vault/file/tokens.txt -## Enable kv -echo 'enable kv' -vault secrets enable -path=secret -version=1 kv -## Enable userpass and add default user -echo 'enable userpass and add default user' -vault auth enable userpass -echo 'enable panel policies' -vault policy write panel-policy /vault/config/panel-policy.hcl -echo 'set password ' -echo ${PANEL_PASS} -vault write auth/userpass/users/admin password=${PANEL_PASS} policies=panel-policy -echo 'check login user/pass' -vault login -method=userpass username=admin password=${PANEL_PASS} - -echo 'enable appid and add default user-id' -vault auth enable app-id -echo 'enable backend policies' -vault policy write backend-policy /vault/config/backend-policy.hcl -echo 'enable backend apps' -vault write auth/app-id/map/app-id/opex-accountant value=backend-policy display_name=opex-accountant -vault write auth/app-id/map/app-id/opex-api value=backend-policy display_name=opex-api -vault write auth/app-id/map/app-id/opex-bc-gateway value=backend-policy display_name=opex-bc-gateway -vault write auth/app-id/map/app-id/opex-eventlog value=backend-policy display_name=opex-eventlog -vault write auth/app-id/map/app-id/opex-auth value=backend-policy display_name=opex-auth -vault write auth/app-id/map/app-id/opex-wallet value=backend-policy display_name=opex-wallet -vault write auth/app-id/map/app-id/opex-websocket value=backend-policy display_name=opex-websocket -vault write auth/app-id/map/app-id/opex-payment value=backend-policy display_name=opex-payment -vault write auth/app-id/map/app-id/opex-admin value=backend-policy display_name=opex-admin -vault write auth/app-id/map/app-id/chain-scan-gateway value=backend-policy display_name=chain-scan-gateway -vault write auth/app-id/map/app-id/opex-referral value=backend-policy display_name=opex-referral -echo 'enable user-id' -vault write auth/app-id/map/user-id/${BACKEND_USER} value=opex-wallet,opex-websocket,opex-eventlog,opex-auth,opex-accountant,opex-api,opex-bc-gateway,opex-payment,opex-admin,chain-scan-gateway,opex-referral -echo 'check login appid' -vault write auth/app-id/login/opex-accountant user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-api user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-bc-gateway user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-eventlog user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-auth user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-wallet user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-websocket user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-payment user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-admin user_id=${BACKEND_USER} -vault write auth/app-id/login/chain-scan-gateway user_id=${BACKEND_USER} -vault write auth/app-id/login/opex-referral user_id=${BACKEND_USER} - -# -## Add secret values -echo 'put key/value' -vault kv put secret/opex smtppass=${SMTP_PASS} -vault kv put secret/opex-accountant dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} -vault kv put secret/opex-api dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} -vault kv put secret/opex-bc-gateway dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} -vault kv put secret/opex-eventlog dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} -vault kv put secret/opex-auth dbusername=${DB_USER} dbpassword=${DB_PASS} admin_username=${KEYCLOAK_ADMIN_USERNAME} admin_password=${KEYCLOAK_ADMIN_PASSWORD} -vault kv put secret/opex-wallet dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} -vault kv put secret/opex-websocket dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} -vault kv put secret/opex-payment dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} vandar_api_key=${VANDAR_API_KEY} -vault kv put secret/opex-admin keycloak_client_secret=${OPEX_ADMIN_KEYCLOAK_CLIENT_SECRET} -vault kv put secret/chain-scan-gateway dbusername=${DB_USER} dbpassword=${DB_PASS} -vault kv put secret/opex-referral dbusername=${DB_USER} dbpassword=${DB_PASS} db_backup_username=${DB_BACKUP_USER} db_backup_pass=${DB_BACKUP_PASS} - -# Keep alive -while pidof vault >/dev/null; do - sleep 10 -done diff --git a/storage/pom.xml b/storage/pom.xml index ff848ecde..c180f6afe 100644 --- a/storage/pom.xml +++ b/storage/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/storage/storage-app/Dockerfile b/storage/storage-app/Dockerfile index 7c71f9447..268392261 100644 --- a/storage/storage-app/Dockerfile +++ b/storage/storage-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/user-management/keycloak-gateway/Dockerfile b/user-management/keycloak-gateway/Dockerfile index 155436997..70f7da628 100644 --- a/user-management/keycloak-gateway/Dockerfile +++ b/user-management/keycloak-gateway/Dockerfile @@ -2,4 +2,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar COPY target/classes/opex-master-realm.json opex-master-realm.json -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KYCStatus.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KYCStatus.kt new file mode 100644 index 000000000..23a0fb0c7 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KYCStatus.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.auth.gateway.data + +enum class KYCStatus { + + REQUESTED, ACCEPTED, REJECTED, NOT_REQUESTED, BLOCKED + +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KYCStatusResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KYCStatusResponse.kt new file mode 100644 index 000000000..c56fd30f4 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KYCStatusResponse.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.auth.gateway.data + +class KYCStatusResponse { + + var status: KYCStatus? = null + var rejectReason: String? = null + + constructor() + + constructor(status: KYCStatus?, rejectReason: String?) { + this.status = status + this.rejectReason = rejectReason + } + +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt index 4903c5d69..8b67229cd 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt @@ -1,7 +1,17 @@ package co.nilin.opex.auth.gateway.data -data class KycRequest( - val selfiePath: String, - val idCardPath: String, - val acceptFormPath: String -) \ No newline at end of file +class KycRequest { + + var selfiePath: String? = null + var idCardPath: String? = null + var acceptFormPath: String? = null + + constructor() + + constructor(selfiePath: String?, idCardPath: String?, acceptFormPath: String?) { + this.selfiePath = selfiePath + this.idCardPath = idCardPath + this.acceptFormPath = acceptFormPath + } + +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt index ca0cb02bd..b91f6b206 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt @@ -15,36 +15,39 @@ class UserProfileInfo { var postalCode: String? = null var residence: String? = null var nationality: String? = null + var address: String? = null constructor() constructor( firstNameEn: String?, lastNameEn: String?, - firstNameFa: String?, - lastNameFa: String?, - birthday: String?, - birthdayJalali: String?, + firstName: String?, + lastName: String?, + birthdayG: String?, + birthdayJ: String?, nationalID: String?, passport: String?, phoneNumber: String?, homeNumber: String?, postalCode: String?, - address: String?, - nationality: String? + residence: String?, + nationality: String?, + address: String? ) { this.firstNameEn = firstNameEn this.lastNameEn = lastNameEn - this.firstName = firstNameFa - this.lastName = lastNameFa - this.birthdayJ = birthday - this.birthdayG = birthdayJalali + this.firstName = firstName + this.lastName = lastName + this.birthdayJ = birthdayJ + this.birthdayG = birthdayG this.nationalId = nationalID this.passportNumber = passport this.mobile = phoneNumber this.telephone = homeNumber this.postalCode = postalCode - this.residence = address + this.residence = residence this.nationality = nationality + this.address = address } } \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/RegistrationOpexCaptcha.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/RegistrationOpexCaptcha.kt index 908d993f4..f0ac26a7d 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/RegistrationOpexCaptcha.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/RegistrationOpexCaptcha.kt @@ -102,8 +102,7 @@ class RegistrationOpexCaptcha : FormAction, FormActionFactory, ConfiguredProvide val httpClient = context.session.getProvider( HttpClientProvider::class.java ).httpClient as CloseableHttpClient - val xForwardedFor = context.httpRequest.httpHeaders.getRequestHeader("X-Forwarded-For") - val proof = "$captcha-${xForwardedFor.first()}" + val proof = "$captcha-${context.connection.remoteAddr}" val post = HttpGet(URIBuilder("http://captcha:8080").addParameter("proof", proof).build()) httpClient.execute(post).use { response -> check(response.statusLine.statusCode / 500 != 5) { "Could not connect to Opex-Captcha service." } diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt index f276bf8e6..1315e8a31 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt @@ -48,15 +48,12 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour @Path("user") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - fun registerUser( - request: RegisterUserRequest, - @HeaderParam("X-Forwarded-For") xForwardedFor: String? - ): Response { + fun registerUser(request: RegisterUserRequest): Response { val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() runCatching { - validateCaptcha("${request.captchaAnswer}-${xForwardedFor?.split(",")?.first() ?: "0.0.0.0"}") + validateCaptcha("${request.captchaAnswer}-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) } @@ -86,14 +83,13 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour @Produces(MediaType.APPLICATION_JSON) fun forgotPassword( @QueryParam("email") email: String?, - @QueryParam("captcha-answer") captchaAnswer: String, - @HeaderParam("X-Forwarded-For") xForwardedFor: String? + @QueryParam("captcha-answer") captchaAnswer: String ): Response { val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() runCatching { - validateCaptcha("$captchaAnswer-${xForwardedFor?.split(",")?.first() ?: "0.0.0.0"}") + validateCaptcha("$captchaAnswer-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) } @@ -263,8 +259,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() val sessions = session.sessions().getUserSessionsStream(opexRealm, user) - .filter { tryOrElse(null) { it.notes["agent"] } != "opex-admin" } - .map { + .filter { tryOrElse(null) { it.notes["agent"] } != "opex-admin" }.map { UserSessionResponse( it.id, it.ipAddress, diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt index 2ebfb37af..c70aa06c6 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt @@ -1,149 +1,227 @@ -package co.nilin.opex.auth.gateway.extension - -import co.nilin.opex.auth.gateway.data.KycRequest -import co.nilin.opex.auth.gateway.data.UserProfileInfo -import co.nilin.opex.auth.gateway.utils.ErrorHandler -import co.nilin.opex.auth.gateway.utils.ResourceAuthenticator -import co.nilin.opex.utility.error.data.OpexError -import org.jboss.resteasy.plugins.providers.multipart.InputPart -import org.keycloak.models.KeycloakSession -import org.keycloak.models.UserModel -import org.keycloak.services.resource.RealmResourceProvider -import org.slf4j.LoggerFactory -import org.springframework.core.io.buffer.DataBuffer -import org.springframework.core.io.buffer.DataBufferUtils -import org.springframework.core.io.buffer.DefaultDataBufferFactory -import reactor.core.publisher.Flux -import java.io.File -import java.nio.file.Paths -import javax.ws.rs.* -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import kotlin.streams.toList - -class UserProfileResource(private val session: KeycloakSession) : RealmResourceProvider { - - private val logger = LoggerFactory.getLogger(UserProfileResource::class.java) - private val opexRealm = session.realms().getRealm("opex") - - @GET - @Path("profile") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - fun getAttributes(): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) - return ErrorHandler.forbidden() - - val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() - return Response.ok(user.attributes).build() - } - - @POST - @Path("profile") - @Consumes(MediaType.APPLICATION_JSON) - fun updateAttributes(request: UserProfileInfo): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) - return ErrorHandler.forbidden() - - val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() - - with(request) { - firstNameEn?.let { user.setSingleAttribute("firstNameEn", it) } - lastNameEn?.let { user.setSingleAttribute("lastNameEn", it) } - firstName?.let { user.setSingleAttribute("firstName", it) } - lastName?.let { user.setSingleAttribute("lastName", it) } - birthdayJ?.let { user.setSingleAttribute("birthdayJ", it) } - birthdayG?.let { user.setSingleAttribute("birthdayG", it) } - nationalId?.let { user.setSingleAttribute("nationalId", it) } - passportNumber?.let { user.setSingleAttribute("passportNumber", it) } - mobile?.let { user.setSingleAttribute("mobile", it) } - telephone?.let { user.setSingleAttribute("telephone", it) } - postalCode?.let { user.setSingleAttribute("postalCode", it) } - residence?.let { user.setSingleAttribute("residence", it) } - nationality?.let { user.setSingleAttribute("nationality", it) } - } - - return Response.noContent().build() - } - - @POST - @Path("profile/kyc") - @Consumes(MediaType.MULTIPART_FORM_DATA) - fun kycFlow(request: KycRequest): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) - return ErrorHandler.forbidden() - - val userId = auth.getUserId() - val user = session.users().getUserById(userId, opexRealm) ?: return ErrorHandler.userNotFound() - - if (isInKycGroups(user)) - return ErrorHandler.response( - Response.Status.BAD_REQUEST, - OpexError.BadRequest, - "User is already in kyc groups" - ) - - /*val forms = input.formDataMap - - val selfiePart = createPartContent(forms["selfie"]?.get(0)!!) - val idPart = createPartContent(forms["idCard"]?.get(0)!!) - val formPart = createPartContent(forms["acceptForm"]?.get(0)!!) - - val selfiePath = proxy.upload(userId, selfiePart).path - val idCard = proxy.upload(userId, idPart).path - val acceptForm = proxy.upload(userId, formPart).path*/ - - - val kycRequestGroup = session.groups() - .getGroupsStream(opexRealm) - .toList() - .find { it.name == "kyc-requested" } - ?: return ErrorHandler.response(Response.Status.NOT_FOUND, OpexError.GroupNotFound) - - user.apply { - joinGroup(kycRequestGroup) - setSingleAttribute("kycSelfiePath", request.selfiePath) - setSingleAttribute("kycIdCardPath", request.idCardPath) - setSingleAttribute("kycAcceptFormPath", request.acceptFormPath) - } - - return Response.noContent().build() - } - - private fun isInKycGroups(user: UserModel): Boolean { - return user.groupsStream.map { it.name } - .filter { it == "kyc-accepted" || it == "kyc-rejected" || it == "kyc-requested" } - .toList() - .isNotEmpty() - } - - private fun createPartContent(input: InputPart): Flux { - val file = input.getBody(File::class.java, null) - val factory = DefaultDataBufferFactory() - return DataBufferUtils.read(Paths.get(file.absolutePath), factory, DEFAULT_BUFFER_SIZE) - -// val fileItem = DiskFileItem( -// "selfie", -// Files.probeContentType(file.toPath()), -// false, -// file.name, -// file.length().toInt(), -// file.parentFile -// ) -// -// FileInputStream(file).use { -// it.transferTo(fileItem.outputStream) -// } - } - - override fun close() { - - } - - override fun getResource(): Any { - return this - } +package co.nilin.opex.auth.gateway.extension + +import co.nilin.opex.auth.gateway.data.KYCStatus +import co.nilin.opex.auth.gateway.data.KYCStatusResponse +import co.nilin.opex.auth.gateway.data.KycRequest +import co.nilin.opex.auth.gateway.data.UserProfileInfo +import co.nilin.opex.auth.gateway.utils.ErrorHandler +import co.nilin.opex.auth.gateway.utils.ResourceAuthenticator +import co.nilin.opex.utility.error.data.OpexError +import org.jboss.resteasy.plugins.providers.multipart.InputPart +import org.keycloak.models.GroupModel +import org.keycloak.models.KeycloakSession +import org.keycloak.models.UserModel +import org.keycloak.services.resource.RealmResourceProvider +import org.slf4j.LoggerFactory +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.io.buffer.DataBufferUtils +import org.springframework.core.io.buffer.DefaultDataBufferFactory +import reactor.core.publisher.Flux +import java.io.File +import java.nio.file.Paths +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import kotlin.streams.toList + +class UserProfileResource(private val session: KeycloakSession) : RealmResourceProvider { + + private val logger = LoggerFactory.getLogger(UserProfileResource::class.java) + private val opexRealm = session.realms().getRealm("opex") + private var kycRequestGroup: GroupModel? = null + private var kycRejectGroup: GroupModel? = null + + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + fun getAttributes(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() + val attributes = mutableMapOf() + user.attributes.entries + .filter { !it.key.startsWith(".") } // Skip hidden attributes + .forEach { + if (it.value.size == 1) + attributes[it.key] = it.value[0] + else if (it.value.size > 1) { + attributes[it.key] = with(StringBuilder()) { + it.value.forEach { v -> append("$v,") } + deleteCharAt(length - 1) + toString() + } + } + } + + return Response.ok(attributes).build() + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + fun updateAttributes(request: UserProfileInfo): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() + + with(request) { + firstNameEn?.let { user.setSingleAttribute("firstNameEn", it) } + lastNameEn?.let { user.setSingleAttribute("lastNameEn", it) } + firstName?.let { user.setSingleAttribute("firstName", it) } + lastName?.let { user.setSingleAttribute("lastName", it) } + birthdayJ?.let { user.setSingleAttribute("birthdayJ", it) } + birthdayG?.let { user.setSingleAttribute("birthdayG", it) } + nationalId?.let { user.setSingleAttribute("nationalId", it) } + passportNumber?.let { user.setSingleAttribute("passportNumber", it) } + mobile?.let { user.setSingleAttribute("mobile", it) } + telephone?.let { user.setSingleAttribute("telephone", it) } + postalCode?.let { user.setSingleAttribute("postalCode", it) } + residence?.let { user.setSingleAttribute("residence", it) } + nationality?.let { user.setSingleAttribute("nationality", it) } + address?.let { user.setSingleAttribute("address", it) } + } + + return Response.noContent().build() + } + + @POST + @Path("kyc") + @Consumes(MediaType.APPLICATION_JSON) + fun kycFlow(request: KycRequest): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val userId = auth.getUserId() + val user = session.users().getUserById(userId, opexRealm) ?: return ErrorHandler.userNotFound() + + if (isInBlockedKycGroups(user)) + return ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexError.UserKYCBlocked + ) + + if (isInNonRetryableKycGroups(user)) + return ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexError.AlreadyInKYC, + "User is already in kyc groups" + ) + + /*val forms = input.formDataMap + + val selfiePart = createPartContent(forms["selfie"]?.get(0)!!) + val idPart = createPartContent(forms["idCard"]?.get(0)!!) + val formPart = createPartContent(forms["acceptForm"]?.get(0)!!) + + val selfiePath = proxy.upload(userId, selfiePart).path + val idCard = proxy.upload(userId, idPart).path + val acceptForm = proxy.upload(userId, formPart).path*/ + + if (kycRequestGroup == null || kycRejectGroup == null) { + val groups = session.groups() + .getGroupsStream(opexRealm) + .toList() + + kycRequestGroup = groups.find { it.name == "kyc-requested" } + kycRejectGroup = groups.find { it.name == "kyc-rejected" } + } + + user.apply { + kycRequestGroup?.let { joinGroup(it) } + kycRejectGroup?.let { leaveGroup(it) } + setSingleAttribute("selfiePath", request.selfiePath) + setSingleAttribute("idCardPath", request.idCardPath) + setSingleAttribute("acceptFormPath", request.acceptFormPath) + } + + return Response.noContent().build() + } + + @GET + @Path("kyc/status") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + fun kycStatus(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val userId = auth.getUserId() + val user = session.users().getUserById(userId, opexRealm) ?: return ErrorHandler.userNotFound() + val status = when (getUserKycGroup(user)) { + "kyc-accepted" -> KYCStatus.ACCEPTED + "kyc-rejected" -> KYCStatus.REJECTED + "kyc-requested" -> KYCStatus.REQUESTED + "kyc-blocked" -> KYCStatus.BLOCKED + else -> KYCStatus.NOT_REQUESTED + } + + val reasonAttr = when (status) { + KYCStatus.REJECTED -> user.attributes[".rejectReason"] + KYCStatus.BLOCKED -> user.attributes[".blockReason"] + else -> null + } + val reason = if (reasonAttr?.isNotEmpty() == true) reasonAttr[0] else null + + return Response.ok(KYCStatusResponse(status, reason)).build() + } + + private fun isInKycGroups(user: UserModel): Boolean { + return user.groupsStream.map { it.name } + .filter { it == "kyc-accepted" || it == "kyc-rejected" || it == "kyc-requested" || it == "kyc-blocked" } + .toList() + .isNotEmpty() + } + + private fun isInNonRetryableKycGroups(user: UserModel): Boolean { + return user.groupsStream.map { it.name } + .filter { it == "kyc-accepted" || it == "kyc-requested" } + .toList() + .isNotEmpty() + } + + private fun isInBlockedKycGroups(user: UserModel): Boolean { + return user.groupsStream.map { it.name } + .filter { it == "kyc-blocked" } + .toList() + .isNotEmpty() + } + + private fun getUserKycGroup(user: UserModel): String? { + val kycGroups = user.groupsStream.map { it.name } + .filter { it == "kyc-accepted" || it == "kyc-rejected" || it == "kyc-requested" || it == "kyc-blocked" } + .toList() + return if (kycGroups.isEmpty()) null else kycGroups[0] + } + + private fun createPartContent(input: InputPart): Flux { + val file = input.getBody(File::class.java, null) + val factory = DefaultDataBufferFactory() + return DataBufferUtils.read(Paths.get(file.absolutePath), factory, DEFAULT_BUFFER_SIZE) + +// val fileItem = DiskFileItem( +// "selfie", +// Files.probeContentType(file.toPath()), +// false, +// file.name, +// file.length().toInt(), +// file.parentFile +// ) +// +// FileInputStream(file).use { +// it.transferTo(fileItem.outputStream) +// } + } + + override fun close() { + + } + + override fun getResource(): Any { + return this + } } \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory index 9ed020e09..fc7affad7 100644 --- a/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +++ b/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -1 +1,2 @@ -co.nilin.opex.auth.gateway.extension.UserManagementResourceFactory \ No newline at end of file +co.nilin.opex.auth.gateway.extension.UserManagementResourceFactory +co.nilin.opex.auth.gateway.extension.UserProfileResourceFactory \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/resources/application.yml b/user-management/keycloak-gateway/src/main/resources/application.yml index 049cb7028..39159e8a6 100644 --- a/user-management/keycloak-gateway/src/main/resources/application.yml +++ b/user-management/keycloak-gateway/src/main/resources/application.yml @@ -1,4 +1,6 @@ -server.port: 8080 +server: + forward-headers-strategy: NATIVE + port: 8080 spring: application: name: opex-auth diff --git a/user-management/keycloak-gateway/src/main/resources/opex-realm.json b/user-management/keycloak-gateway/src/main/resources/opex-realm.json index 68e2e6968..209292e34 100644 --- a/user-management/keycloak-gateway/src/main/resources/opex-realm.json +++ b/user-management/keycloak-gateway/src/main/resources/opex-realm.json @@ -4,10 +4,10 @@ "notBefore": 0, "revokeRefreshToken": false, "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, + "accessTokenLifespan": 1800, "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeout": 1209600, + "ssoSessionMaxLifespan": 1209600, "ssoSessionIdleTimeoutRememberMe": 0, "ssoSessionMaxLifespanRememberMe": 0, "offlineSessionIdleTimeout": 2592000, @@ -446,6 +446,15 @@ "clientRoles": {}, "subGroups": [] }, + { + "id": "0b4726fc-84b7-4db6-a76d-b77f5254ba4d", + "name": "kyc-blocked", + "path": "/kyc-blocked", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + }, { "id": "abf74cdd-09c9-473f-9438-f6e545571c68", "name": "kyc-rejected", @@ -700,7 +709,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "3d5dc66b-5576-4b62-87cf-1c2b008046d4", "defaultRoles": [ "manage-account", "view-profile" @@ -768,7 +777,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "8a3fa345-2543-4070-871d-2c19f1e8a59d", "redirectUris": [ "/realms/opex/account/*", "http://localhost:3000/*", @@ -887,7 +896,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "0247ee8c-ca5a-4f68-a3a7-c668aaa05751", "redirectUris": [], "webOrigins": [ "*" @@ -904,7 +913,6 @@ "protocol": "openid-connect", "attributes": { "saml.assertion.signature": "false", - "access.token.lifespan": "3600", "saml.multivalued.roles": "false", "saml.force.post.binding": "false", "saml.encrypt": "false", @@ -1023,7 +1031,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "a8fae04d-7966-46eb-b9f2-ccc37b7d6862", "redirectUris": [ "http://localhost:3000/*", "https://opex.dev/*" @@ -1086,7 +1094,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "c538c83a-9c2a-4758-abae-046db42aacdc", "redirectUris": [], "webOrigins": [], "notBefore": 0, @@ -1142,7 +1150,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "b25cd2c3-1bbd-4988-ab6a-8439ac4e2c60", "redirectUris": [ "http://localhost:8082/new-client/login/oauth2/code/custom", "http://localhost:3000/*", @@ -1209,7 +1217,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "b01a5a5a-5f99-4efb-9189-1dfb1798850f", "redirectUris": [ "*" ], @@ -1324,7 +1332,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "7abe18a6-408f-4918-b620-bca4bfe9e48a", "redirectUris": [], "webOrigins": [], "notBefore": 0, @@ -1904,7 +1912,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "10594c9f-3666-4e35-bc34-4a3de5ab9c0d", "redirectUris": [ "/admin/opex/console/*" ], @@ -1982,7 +1990,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "d3d927e4-6ad6-4545-9f6f-0725a79b2d9e", "redirectUris": [ "*" ], diff --git a/user-management/pom.xml b/user-management/pom.xml index 1c7bb0d0f..febfd0efd 100644 --- a/user-management/pom.xml +++ b/user-management/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index 258a0b6cc..06400af37 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -1,80 +1,83 @@ -package co.nilin.opex.utility.error.data - -import org.springframework.http.HttpStatus - -enum class OpexError(val code: Int, val message: String?, val status: HttpStatus) { - - // Code 1000: general - Error(1000, "Generic error", HttpStatus.INTERNAL_SERVER_ERROR), - InternalServerError(1001, "Internal server error", HttpStatus.INTERNAL_SERVER_ERROR), - BadRequest(1002, "Bad request", HttpStatus.BAD_REQUEST), - UnAuthorized(1003, "Unauthorized", HttpStatus.UNAUTHORIZED), - Forbidden(1004, "Forbidden", HttpStatus.FORBIDDEN), - NotFound(1005, "Not found", HttpStatus.NOT_FOUND), - InvalidRequestParam(1020, "Parameter '%s' is either missing or invalid", HttpStatus.BAD_REQUEST), - InvalidRequestBody(1021, "Request body is invalid", HttpStatus.BAD_REQUEST), - - // code 2000: accountant - InvalidPair(2001, "%s is not available", HttpStatus.BAD_REQUEST), - InvalidPairFee(2002, "%s fee is not available", HttpStatus.BAD_REQUEST), - - // code 3000: matching-engine - - // code 4000: matching-gateway - SubmitOrderForbiddenByAccountant(4001, null, HttpStatus.BAD_REQUEST), - - // code 5000: user-management - EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST), - GroupNotFound(5002, "Group not found", HttpStatus.NOT_FOUND), - OTPAlreadyEnabled(5003, "2FA/OTP already configured", HttpStatus.BAD_REQUEST), - UserNotFound(5004, "User not found", HttpStatus.NOT_FOUND), - InvalidOTP(5005, "Invalid OTP", HttpStatus.FORBIDDEN), - OTPRequired(5006, "OTP Required", HttpStatus.BAD_REQUEST), - - // code 6000: wallet - WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), - WalletNotFound(6002, null, HttpStatus.NOT_FOUND), - CurrencyNotFound(6003, null, HttpStatus.NOT_FOUND), - InvalidCashOutUsage(6004, "Use withdraw services", HttpStatus.BAD_REQUEST), - - // code 7000: api - OrderNotFound(7001, "No order found", HttpStatus.NOT_FOUND), - SymbolNotFound(7002, "No symbol found", HttpStatus.NOT_FOUND), - InvalidLimitForOrderBook(7003, "Valid limits: [5, 10, 20, 50, 100, 500, 1000, 5000]", HttpStatus.BAD_REQUEST), - InvalidLimitForRecentTrades(7004, "Valid limits: 1 min - 1000 max", HttpStatus.BAD_REQUEST), - InvalidPriceChangeDuration(7005, "Valid durations: [24h, 7d, 1m]", HttpStatus.BAD_REQUEST), - CancelOrderNotAllowed(7006, "Canceling this order is not allowed", HttpStatus.FORBIDDEN), - InvalidInterval(7007, "Invalid interval", HttpStatus.BAD_REQUEST), - - // code 8000: bc-gateway - ReservedAddressNotAvailable(8001, "No reserved address available", HttpStatus.BAD_REQUEST), - DuplicateToken(8002, "Asset already exists", HttpStatus.BAD_REQUEST), - ChainNotFound(8003, "Chain not found", HttpStatus.NOT_FOUND), - CurrencyNotFoundBC(8004, "Currency not found", HttpStatus.NOT_FOUND), - TokenNotFound(8005, "Coin/Token not found", HttpStatus.NOT_FOUND), - InvalidAddressType(8006, "Address type is invalid", HttpStatus.NOT_FOUND), - - // code 9000: admin - UserNotFoundAdmin(9001, "User not found", HttpStatus.NOT_FOUND), - - // code 10000: bc-gateway - InvalidCaptcha(10001, "Captcha is not valid", HttpStatus.BAD_REQUEST); - - companion object { - fun findByCode(code: Int?): OpexError? { - code ?: return null - return values().find { it.code == code } - } - } - -} - -@Throws(OpexException::class) -inline fun T.throwError( - error: OpexError, - message: String? = null, - status: HttpStatus? = null, - data: Any? = null -) { - throw OpexException(error, message, status, data, T::class.java) +package co.nilin.opex.utility.error.data + +import org.springframework.http.HttpStatus + +enum class OpexError(val code: Int, val message: String?, val status: HttpStatus) { + + // Code 1000: general + Error(1000, "Generic error", HttpStatus.INTERNAL_SERVER_ERROR), + InternalServerError(1001, "Internal server error", HttpStatus.INTERNAL_SERVER_ERROR), + BadRequest(1002, "Bad request", HttpStatus.BAD_REQUEST), + UnAuthorized(1003, "Unauthorized", HttpStatus.UNAUTHORIZED), + Forbidden(1004, "Forbidden", HttpStatus.FORBIDDEN), + NotFound(1005, "Not found", HttpStatus.NOT_FOUND), + ServiceUnavailable(1006, null, HttpStatus.SERVICE_UNAVAILABLE), + InvalidRequestParam(1020, "Parameter '%s' is either missing or invalid", HttpStatus.BAD_REQUEST), + InvalidRequestBody(1021, "Request body is invalid", HttpStatus.BAD_REQUEST), + + // code 2000: accountant + InvalidPair(2001, "%s is not available", HttpStatus.BAD_REQUEST), + InvalidPairFee(2002, "%s fee is not available", HttpStatus.BAD_REQUEST), + + // code 3000: matching-engine + + // code 4000: matching-gateway + SubmitOrderForbiddenByAccountant(4001, null, HttpStatus.BAD_REQUEST), + + // code 5000: user-management + EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST), + GroupNotFound(5002, "Group not found", HttpStatus.NOT_FOUND), + OTPAlreadyEnabled(5003, "2FA/OTP already configured", HttpStatus.BAD_REQUEST), + UserNotFound(5004, "User not found", HttpStatus.NOT_FOUND), + InvalidOTP(5005, "Invalid OTP", HttpStatus.FORBIDDEN), + OTPRequired(5006, "OTP Required", HttpStatus.BAD_REQUEST), + AlreadyInKYC(5007, "KYC flow for this user has executed", HttpStatus.BAD_REQUEST), + UserKYCBlocked(5008, "User is blocked from KYC", HttpStatus.BAD_REQUEST), + + // code 6000: wallet + WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), + WalletNotFound(6002, null, HttpStatus.NOT_FOUND), + CurrencyNotFound(6003, null, HttpStatus.NOT_FOUND), + InvalidCashOutUsage(6004, "Use withdraw services", HttpStatus.BAD_REQUEST), + + // code 7000: api + OrderNotFound(7001, "No order found", HttpStatus.NOT_FOUND), + SymbolNotFound(7002, "No symbol found", HttpStatus.NOT_FOUND), + InvalidLimitForOrderBook(7003, "Valid limits: [5, 10, 20, 50, 100, 500, 1000, 5000]", HttpStatus.BAD_REQUEST), + InvalidLimitForRecentTrades(7004, "Valid limits: 1 min - 1000 max", HttpStatus.BAD_REQUEST), + InvalidPriceChangeDuration(7005, "Valid durations: [24h, 7d, 1m]", HttpStatus.BAD_REQUEST), + CancelOrderNotAllowed(7006, "Canceling this order is not allowed", HttpStatus.FORBIDDEN), + InvalidInterval(7007, "Invalid interval", HttpStatus.BAD_REQUEST), + + // code 8000: bc-gateway + ReservedAddressNotAvailable(8001, "No reserved address available", HttpStatus.BAD_REQUEST), + DuplicateToken(8002, "Asset already exists", HttpStatus.BAD_REQUEST), + ChainNotFound(8003, "Chain not found", HttpStatus.NOT_FOUND), + CurrencyNotFoundBC(8004, "Currency not found", HttpStatus.NOT_FOUND), + TokenNotFound(8005, "Coin/Token not found", HttpStatus.NOT_FOUND), + InvalidAddressType(8006, "Address type is invalid", HttpStatus.NOT_FOUND), + + // code 9000: admin + UserNotFoundAdmin(9001, "User not found", HttpStatus.NOT_FOUND), + + // code 10000: bc-gateway + InvalidCaptcha(10001, "Captcha is not valid", HttpStatus.BAD_REQUEST); + + companion object { + fun findByCode(code: Int?): OpexError? { + code ?: return null + return values().find { it.code == code } + } + } + +} + +@Throws(OpexException::class) +inline fun T.throwError( + error: OpexError, + message: String? = null, + status: HttpStatus? = null, + data: Any? = null +) { + throw OpexException(error, message, status, data, T::class.java) } \ No newline at end of file diff --git a/utility/pom.xml b/utility/pom.xml index 9200d8b7c..6216eec86 100644 --- a/utility/pom.xml +++ b/utility/pom.xml @@ -5,7 +5,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -20,6 +20,7 @@ error-handler logging-handler interceptors + preferences @@ -42,4 +43,4 @@ - \ No newline at end of file + diff --git a/utility/preferences/pom.xml b/utility/preferences/pom.xml new file mode 100644 index 000000000..6dfc536ef --- /dev/null +++ b/utility/preferences/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + utility + co.nilin.opex.utility + 1.0-SNAPSHOT + + + co.nilin.opex.utility.preferences + preferences + + + + org.springframework + spring-context + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.13.0 + + + diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/AddressType.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/AddressType.kt new file mode 100644 index 000000000..7ef22eba0 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/AddressType.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.utility.preferences + +data class AddressType(var addressType: String = "", var addressRegex: String = "*") diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Alias.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Alias.kt new file mode 100644 index 000000000..4fbd6b4a8 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Alias.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.utility.preferences + +data class Alias(var key: String = "", var alias: String = "") diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Chain.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Chain.kt new file mode 100644 index 000000000..8cc4abecc --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Chain.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.utility.preferences + +data class Chain( + var name: String = "", + var addressType: String = "", + val endpointUrl: String = "", + var schedule: ChainSyncSchedule = ChainSyncSchedule() +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/ChainSyncSchedule.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/ChainSyncSchedule.kt new file mode 100644 index 000000000..19b8f824a --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/ChainSyncSchedule.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.utility.preferences + +data class ChainSyncSchedule( + var delay: Long = 600, var errorDelay: Long = 60 +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Currency.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Currency.kt new file mode 100644 index 000000000..1537c7583 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Currency.kt @@ -0,0 +1,16 @@ +package co.nilin.opex.utility.preferences + +import java.math.BigDecimal + +data class Currency( + var symbol: String = "", + var name: String = "", + var precision: BigDecimal = BigDecimal.ONE, + var mainBalance: BigDecimal = BigDecimal.ZERO, + var dailyTotal: BigDecimal = BigDecimal.valueOf(1000), + var dailyCount: Int = 100, + var monthlyTotal: BigDecimal = BigDecimal.valueOf(30000), + var monthlyCount: Int = 3000, + var implementations: List = emptyList(), + var gift: BigDecimal = BigDecimal.ZERO +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/CurrencyImplementation.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/CurrencyImplementation.kt new file mode 100644 index 000000000..97d778095 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/CurrencyImplementation.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.utility.preferences + +import java.math.BigDecimal + +data class CurrencyImplementation( + var chain: String = "", + var withdrawEnabled: Boolean = true, + var token: Boolean = false, + var tokenAddress: String? = null, + var tokenName: String? = null, + var withdrawFee: BigDecimal = BigDecimal.valueOf(0.01), + var withdrawMin: BigDecimal = BigDecimal.valueOf(0.01), + var decimal: Int = 0 +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/FeeConfig.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/FeeConfig.kt new file mode 100644 index 000000000..44e10c193 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/FeeConfig.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.utility.preferences + +import java.math.BigDecimal + +data class FeeConfig( + var userLevel: String = "", + var direction: String = "", + var makerFee: BigDecimal = BigDecimal.valueOf(0.01), + var takerFee: BigDecimal = BigDecimal.valueOf(0.01) +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Market.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Market.kt new file mode 100644 index 000000000..04c738c88 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Market.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.utility.preferences + +import java.math.BigDecimal + +data class Market( + var leftSide: String = "", + var rightSide: String = "", + var pair: String? = null, + var feeConfigs: List = emptyList(), + var aliases: List = emptyList(), + var leftSideFraction: BigDecimal? = null, + var rightSideFraction: BigDecimal? = null +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt new file mode 100644 index 000000000..5649f35d2 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.utility.preferences + +data class Preferences( + var addressTypes: List = emptyList(), + var chains: List = emptyList(), + var currencies: List = emptyList(), + var markets: List = emptyList(), + var userLimits: List = emptyList(), + var wallet: Wallet = Wallet(), + var system: System = System() +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/System.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/System.kt new file mode 100644 index 000000000..7820c402d --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/System.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.utility.preferences + +data class System( + var walletTitle: String = "system", var walletLevel: String = "basic" +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/UserLimit.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/UserLimit.kt new file mode 100644 index 000000000..958374899 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/UserLimit.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.utility.preferences + +import java.math.BigDecimal + +data class UserLimit( + var level: String? = null, + var owner: Long = 1, + var action: String = "withdraw", + var walletType: String = "main", + var withdrawFee: BigDecimal = BigDecimal.valueOf(0.0001), + var dailyTotal: BigDecimal = BigDecimal.valueOf(1000), + var dailyCount: Int = 100, + var monthlyTotal: BigDecimal = BigDecimal.valueOf(30000), + var monthlyCount: Int = 3000 +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Wallet.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Wallet.kt new file mode 100644 index 000000000..487130f88 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Wallet.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.utility.preferences + +data class Wallet( + var schedule: WalletSyncSchedule = WalletSyncSchedule() +) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/WalletSyncSchedule.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/WalletSyncSchedule.kt new file mode 100644 index 000000000..d001817e2 --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/WalletSyncSchedule.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.utility.preferences + +class WalletSyncSchedule(var delay: Long = 10, var batchSize: Long = 10000) diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/reader/ReadPreferences.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/reader/ReadPreferences.kt new file mode 100644 index 000000000..cce06f5cd --- /dev/null +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/reader/ReadPreferences.kt @@ -0,0 +1,23 @@ +package co.nilin.opex.utility.preferences.reader + +import co.nilin.opex.utility.preferences.Preferences +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.io.File + +@Configuration +class ReadPreferences() { + private val mapper = ObjectMapper(YAMLFactory()) + + @Value("\${PREFERENCES:classpath:preferences.yml}") + private lateinit var preferencesYmlPath: String + + @Bean + fun preferences(): Preferences = runCatching { + val preferencesYml = File(preferencesYmlPath) + mapper.readValue(preferencesYml, Preferences::class.java) + }.getOrElse { Preferences() } +} diff --git a/wallet/pom.xml b/wallet/pom.xml index 4dbd94f73..79ffc0cc5 100644 --- a/wallet/pom.xml +++ b/wallet/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -61,6 +61,11 @@ interceptors ${project.version} + + co.nilin.opex.utility.preferences + preferences + ${project.version} + diff --git a/wallet/wallet-app/Dockerfile b/wallet/wallet-app/Dockerfile index 7c71f9447..268392261 100644 --- a/wallet/wallet-app/Dockerfile +++ b/wallet/wallet-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/wallet/wallet-app/pom.xml b/wallet/wallet-app/pom.xml index f38284053..f3fa59979 100644 --- a/wallet/wallet-app/pom.xml +++ b/wallet/wallet-app/pom.xml @@ -107,6 +107,10 @@ org.springframework.cloud spring-cloud-starter-vault-config + + co.nilin.opex.utility.preferences + preferences + diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/InitializeService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/InitializeService.kt new file mode 100644 index 000000000..77f544b9f --- /dev/null +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/InitializeService.kt @@ -0,0 +1,84 @@ +package co.nilin.opex.wallet.app.config + +import co.nilin.opex.utility.preferences.Currency +import co.nilin.opex.utility.preferences.Preferences +import co.nilin.opex.utility.preferences.UserLimit +import co.nilin.opex.wallet.ports.postgres.dao.CurrencyRepository +import co.nilin.opex.wallet.ports.postgres.dao.UserLimitsRepository +import co.nilin.opex.wallet.ports.postgres.dao.WalletOwnerRepository +import co.nilin.opex.wallet.ports.postgres.dao.WalletRepository +import co.nilin.opex.wallet.ports.postgres.model.UserLimitsModel +import co.nilin.opex.wallet.ports.postgres.model.WalletModel +import co.nilin.opex.wallet.ports.postgres.model.WalletOwnerModel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.DependsOn +import org.springframework.stereotype.Component +import java.math.BigDecimal +import javax.annotation.PostConstruct + +@Component +@DependsOn("postgresConfig") +class InitializeService( + @Value("\${app.system.uuid}") val systemUuid: String, + private val currencyRepository: CurrencyRepository, + private val walletOwnerRepository: WalletOwnerRepository, + private val walletRepository: WalletRepository, + private val userLimitsRepository: UserLimitsRepository +) { + @Autowired + private lateinit var preferences: Preferences + + @PostConstruct + fun init() = runBlocking { + addCurrencies(preferences.currencies) + addSystemWallet(preferences) + addUserLimits(preferences.userLimits) + } + + private suspend fun addUserLimits(data: List) = coroutineScope { + data.forEachIndexed { i, it -> + if (!userLimitsRepository.existsById(i + 1L).awaitSingle()) { + runCatching { + userLimitsRepository.save( + UserLimitsModel( + null, + it.level, + it.owner, + it.action, + it.walletType, + it.dailyTotal, + it.dailyCount, + it.monthlyTotal, + it.monthlyCount + ) + ).awaitSingleOrNull() + } + } + } + } + + private suspend fun addSystemWallet(p: Preferences) = coroutineScope { + if (!walletOwnerRepository.existsById(1).awaitSingle()) { + walletOwnerRepository.save(WalletOwnerModel(null, systemUuid, p.system.walletTitle, p.system.walletLevel)) + .awaitSingleOrNull() + } + val items = p.currencies.flatMap { + listOf( + WalletModel(null, 1, "main", it.symbol, it.mainBalance), + WalletModel(null, 1, "exchange", it.symbol, BigDecimal.ZERO) + ) + } + runCatching { walletRepository.saveAll(items).collectList().awaitSingleOrNull() } + } + + private suspend fun addCurrencies(data: List) = coroutineScope { + data.forEach { + currencyRepository.insert(it.name, it.symbol, it.precision.toDouble()).awaitSingleOrNull() + } + } +} diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/UserRegistrationService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/UserRegistrationService.kt index 4ee4c420e..f2c679c81 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/UserRegistrationService.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/UserRegistrationService.kt @@ -2,47 +2,31 @@ package co.nilin.opex.wallet.app.service import co.nilin.opex.utility.error.data.OpexError import co.nilin.opex.utility.error.data.OpexException +import co.nilin.opex.utility.preferences.Preferences import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.model.Wallet import co.nilin.opex.wallet.core.spi.CurrencyService import co.nilin.opex.wallet.core.spi.WalletManager import co.nilin.opex.wallet.core.spi.WalletOwnerManager import co.nilin.opex.wallet.ports.kafka.listener.model.UserCreatedEvent -import org.springframework.beans.factory.annotation.Value +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal @Component class UserRegistrationService( - val walletOwnerManager: WalletOwnerManager, - val walletManager: WalletManager, - val currencyService: CurrencyService, - @Value("\${app.gift.symbol}") - val symbol: String, - @Value("\${app.gift.amount}") - val amount: BigDecimal + val walletOwnerManager: WalletOwnerManager, val walletManager: WalletManager, val currencyService: CurrencyService ) { - @Transactional - suspend fun registerNewUser(event: UserCreatedEvent): Wallet { - val owner = - walletOwnerManager.createWalletOwner(event.uuid, "${event.firstName} ${event.lastName}", "1") + @Autowired + private lateinit var preferences: Preferences - val btcSymbol = currencyService.getCurrency("tbtc") ?: throw OpexException(OpexError.CurrencyNotFound) - //TODO REMOVE LATER - walletManager.createWallet( - owner, - Amount(btcSymbol, BigDecimal.ONE), - btcSymbol, - "main" - ) + @Transactional + suspend fun registerNewUser(event: UserCreatedEvent) { + val owner = walletOwnerManager.createWalletOwner(event.uuid, "${event.firstName} ${event.lastName}", "1") - val giftSymbol = currencyService.getCurrency(symbol) ?: throw OpexException(OpexError.CurrencyNotFound) - return walletManager.createWallet( - owner, - Amount(giftSymbol, amount), - giftSymbol, - "main" - ) + preferences.currencies.filter { it.gift > BigDecimal.ZERO }.forEach { + val currency = currencyService.getCurrency(it.symbol) ?: throw OpexException(OpexError.CurrencyNotFound) + walletManager.createWallet(owner, Amount(currency, it.gift), currency, "main") + } } } diff --git a/wallet/wallet-app/src/main/resources/application.yml b/wallet/wallet-app/src/main/resources/application.yml index 6874669f9..2f3f0f75e 100644 --- a/wallet/wallet-app/src/main/resources/application.yml +++ b/wallet/wallet-app/src/main/resources/application.yml @@ -44,9 +44,6 @@ spring: config: import: vault://secret/${spring.application.name} app: - gift: - symbol: tusdt - amount: 1000 auth: cert-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/certs system: diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt index d60441401..ea5cb334f 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt @@ -1,91 +1,90 @@ -package co.nilin.opex.wallet.core.service - -import co.nilin.opex.wallet.core.exc.CurrencyNotMatchedException -import co.nilin.opex.wallet.core.exc.DepositLimitExceededException -import co.nilin.opex.wallet.core.exc.NotEnoughBalanceException -import co.nilin.opex.wallet.core.exc.WithdrawLimitExceededException -import co.nilin.opex.wallet.core.inout.TransferCommand -import co.nilin.opex.wallet.core.inout.TransferResult -import co.nilin.opex.wallet.core.inout.TransferResultDetailed -import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.model.Transaction -import co.nilin.opex.wallet.core.spi.* -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime -import java.util.* - -@Service -class TransferService( - val currencyRateService: CurrencyRateService, - val walletManager: WalletManager, - val walletListener: WalletListener, - val walletOwnerManager: WalletOwnerManager, - val transactionManager: TransactionManager -) { - @Transactional - suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed { - //pre transfer hook (dispatch pre transfer event) - val srcWallet = transferCommand.sourceWallet - val srcWalletOwner = srcWallet.owner() - val srcWalletBalance = srcWallet.balance() - if (srcWallet.currency() != transferCommand.amount.currency) - throw CurrencyNotMatchedException() - if (srcWalletBalance.amount < transferCommand.amount.amount) - throw NotEnoughBalanceException() - if (!walletOwnerManager.isWithdrawAllowed(srcWalletOwner, transferCommand.amount)) - throw WithdrawLimitExceededException() - if (!walletManager.isWithdrawAllowed(srcWallet, transferCommand.amount.amount)) - throw WithdrawLimitExceededException() - - val destWallet = transferCommand.destWallet - val destWalletOwner = destWallet.owner() - //check wallet if can accept the value type - val amountToTransfer = currencyRateService.convert(transferCommand.amount, destWallet.currency()) - - if (!walletOwnerManager.isDepositAllowed(destWalletOwner, Amount(destWallet.currency(), amountToTransfer))) - throw DepositLimitExceededException() - if (!walletManager.isDepositAllowed(destWallet, amountToTransfer)) - throw DepositLimitExceededException() - - walletManager.decreaseBalance(srcWallet, transferCommand.amount.amount) - walletManager.increaseBalance(destWallet, amountToTransfer) - val tx = transactionManager.save( - Transaction( - srcWallet, - destWallet, - transferCommand.amount.amount, - amountToTransfer, - transferCommand.description, - transferCommand.transferRef, - LocalDateTime.now() - ) - ) - //get the result and add to return result type - walletListener.onDeposit( - destWallet, - srcWallet, - transferCommand.amount, - amountToTransfer, - tx, - transferCommand.additionalData - ) - walletListener.onWithdraw(srcWallet, destWallet, transferCommand.amount, tx, transferCommand.additionalData) - //post transfer hook(dispatch post transfer event) - - //notify balance change - return TransferResultDetailed( - TransferResult( - Date().time, - srcWalletOwner.uuid(), - srcWallet.type(), - srcWalletBalance, - walletManager.findWalletById(srcWallet.id()!!)!!.balance(), - transferCommand.amount, - destWalletOwner.uuid(), - destWallet.type(), - Amount(destWallet.currency(), amountToTransfer) - ), tx - ) - } +package co.nilin.opex.wallet.core.service + +import co.nilin.opex.wallet.core.exc.CurrencyNotMatchedException +import co.nilin.opex.wallet.core.exc.DepositLimitExceededException +import co.nilin.opex.wallet.core.exc.NotEnoughBalanceException +import co.nilin.opex.wallet.core.exc.WithdrawLimitExceededException +import co.nilin.opex.wallet.core.inout.TransferCommand +import co.nilin.opex.wallet.core.inout.TransferResult +import co.nilin.opex.wallet.core.inout.TransferResultDetailed +import co.nilin.opex.wallet.core.model.Amount +import co.nilin.opex.wallet.core.model.Transaction +import co.nilin.opex.wallet.core.spi.* +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.* + +@Service +class TransferService( + val walletManager: WalletManager, + val walletListener: WalletListener, + val walletOwnerManager: WalletOwnerManager, + val transactionManager: TransactionManager +) { + @Transactional + suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed { + //pre transfer hook (dispatch pre transfer event) + val srcWallet = transferCommand.sourceWallet + val srcWalletOwner = srcWallet.owner() + val srcWalletBalance = srcWallet.balance() + if (srcWallet.currency() != transferCommand.amount.currency) + throw CurrencyNotMatchedException() + if (srcWalletBalance.amount < transferCommand.amount.amount) + throw NotEnoughBalanceException() + if (!walletOwnerManager.isWithdrawAllowed(srcWalletOwner, transferCommand.amount)) + throw WithdrawLimitExceededException() + if (!walletManager.isWithdrawAllowed(srcWallet, transferCommand.amount.amount)) + throw WithdrawLimitExceededException() + + val destWallet = transferCommand.destWallet + val destWalletOwner = destWallet.owner() + //check wallet if can accept the value type + val amountToTransfer = transferCommand.amount.amount + + if (!walletOwnerManager.isDepositAllowed(destWalletOwner, Amount(destWallet.currency(), amountToTransfer))) + throw DepositLimitExceededException() + if (!walletManager.isDepositAllowed(destWallet, amountToTransfer)) + throw DepositLimitExceededException() + + walletManager.decreaseBalance(srcWallet, transferCommand.amount.amount) + walletManager.increaseBalance(destWallet, amountToTransfer) + val tx = transactionManager.save( + Transaction( + srcWallet, + destWallet, + transferCommand.amount.amount, + amountToTransfer, + transferCommand.description, + transferCommand.transferRef, + LocalDateTime.now() + ) + ) + //get the result and add to return result type + walletListener.onDeposit( + destWallet, + srcWallet, + transferCommand.amount, + amountToTransfer, + tx, + transferCommand.additionalData + ) + walletListener.onWithdraw(srcWallet, destWallet, transferCommand.amount, tx, transferCommand.additionalData) + //post transfer hook(dispatch post transfer event) + + //notify balance change + return TransferResultDetailed( + TransferResult( + Date().time, + srcWalletOwner.uuid(), + srcWallet.type(), + srcWalletBalance, + walletManager.findWalletById(srcWallet.id()!!)!!.balance(), + transferCommand.amount, + destWalletOwner.uuid(), + destWallet.type(), + Amount(destWallet.currency(), amountToTransfer) + ), tx + ) + } } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/config/PostgresConfig.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/config/PostgresConfig.kt index 793b4a09a..d06b8686d 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/config/PostgresConfig.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/config/PostgresConfig.kt @@ -10,17 +10,13 @@ import org.springframework.r2dbc.core.DatabaseClient @EnableR2dbcRepositories(basePackages = ["co.nilin.opex"]) class PostgresConfig( db: DatabaseClient, - @Value("classpath:schema.sql") private val schemaResource: Resource, - @Value("classpath:data.sql") private val dataResource: Resource? + @Value("classpath:schema.sql") private val schemaResource: Resource ) { init { val schemaReader = schemaResource.inputStream.reader() val schema = schemaReader.readText().trim() schemaReader.close() - val dataReader = dataResource?.inputStream?.reader() - val data = dataReader?.readText()?.trim() ?: "" - dataReader?.close() - val initDb = db.sql { schema.plus(data) } + val initDb = db.sql { schema } initDb // initialize the database .then() .subscribe() // execute diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt deleted file mode 100644 index 27625d195..000000000 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package co.nilin.opex.wallet.ports.postgres.dao - -import co.nilin.opex.wallet.ports.postgres.model.CurrencyRateModel -import org.springframework.data.r2dbc.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Mono - -@Repository -interface CurrencyRateRepository : ReactiveCrudRepository { - @Query("select * from currency_rate where source_currency = :sourceCurrency and dest_currency = :destCurrency") - fun findBySourceAndDest( - @Param("source") sourceCurrency: String, @Param("dest") destCurrency: String - ): Mono -} diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepository.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepository.kt index dcc8981a2..53700ce0b 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepository.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepository.kt @@ -12,10 +12,10 @@ interface CurrencyRepository : ReactiveCrudRepository { @Query("select * from currency where symbol = :symbol") fun findBySymbol(symbol: String): Mono - @Query("insert into currency values (:name, :symbol, :precision) on conflict do nothing") + @Query("insert into currency values (:symbol, :name, :precision) on conflict do nothing") fun insert(name: String, symbol: String, precision: Double): Mono @Query("delete from currency where name = :name") fun deleteByName(name: String): Mono -} \ No newline at end of file +} diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt deleted file mode 100644 index df8d8bbab..000000000 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -package co.nilin.opex.wallet.ports.postgres.impl - -import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.model.Currency -import co.nilin.opex.wallet.core.spi.CurrencyRateService -import co.nilin.opex.wallet.ports.postgres.dao.CurrencyRateRepository -import kotlinx.coroutines.reactive.awaitFirstOrDefault -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Service -import java.math.BigDecimal - -@Service -class CurrencyRateServiceImpl(val currencyRateRepository: CurrencyRateRepository) : CurrencyRateService { - override suspend fun convert(amount: Amount, targetCurrency: Currency): BigDecimal { - if (amount.currency.getSymbol() == targetCurrency.getSymbol()) - return amount.amount - - var rate = currencyRateRepository.findBySourceAndDest( - amount.currency.getSymbol(), targetCurrency.getSymbol() - ) - .map { BigDecimal.valueOf(it!!.rate) } - .awaitFirstOrNull() - if (rate != null) { - rate = currencyRateRepository.findBySourceAndDest( - targetCurrency.getSymbol(), amount.currency.getSymbol() - ) - .map { BigDecimal.valueOf(it!!.rate) } - .awaitFirstOrDefault(BigDecimal.ZERO) - } - return amount.amount.multiply(rate) - } -} \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/CurrencyModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/CurrencyModel.kt index cf4f253b0..3218f81de 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/CurrencyModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/CurrencyModel.kt @@ -9,8 +9,8 @@ import org.springframework.data.relational.core.mapping.Table @Table("currency") data class CurrencyModel( - @JsonIgnore @Id @Column("name") val name_: String, - @JsonIgnore @Column("symbol") var symbol_: String, + @JsonIgnore @Id @Column("symbol") var symbol_: String, + @JsonIgnore @Column("name") val name_: String, @JsonIgnore @Column("precision") var precision_: Double ) : Currency { diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/UserLimitsModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/UserLimitsModel.kt index edc298c78..cb70e3379 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/UserLimitsModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/UserLimitsModel.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal @Table("user_limits") class UserLimitsModel( - @Id val id: Long?, + @Id var id: Long?, val level: String?, val owner: Long?, val action: String, //withdraw or deposit @@ -16,4 +16,4 @@ class UserLimitsModel( @Column("daily_count") val dailyCount: Int?, @Column("monthly_total") val monthlyTotal: BigDecimal?, @Column("monthly_count") val monthlyCount: Int? -) \ No newline at end of file +) diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/data.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/data.sql deleted file mode 100644 index b6ef6f729..000000000 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/data.sql +++ /dev/null @@ -1,76 +0,0 @@ -INSERT INTO wallet_owner(id, uuid, title, level) -VALUES (1, '1', 'system', 'basic') -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('wallet_owner', 'id'), (SELECT MAX(id) FROM wallet_owner)); - -INSERT INTO currency(name, symbol, precision) -VALUES ('btc', 'btc', 0.000001), - ('eth', 'eth', 0.00001), - ('usdt', 'usdt', 0.01), - ('nln', 'nln', 1), - ('IRT', 'IRT', 1) -ON CONFLICT DO NOTHING; - --- Test currency -INSERT INTO currency(name, symbol, precision) -VALUES ('tbtc', 'tbtc', 0.000001), - ('teth', 'teth', 0.00001), - ('tusdt', 'tusdt', 0.01) -ON CONFLICT DO NOTHING; - -INSERT INTO currency_rate(id, source_currency, dest_currency, rate) -VALUES (1, 'btc', 'nln', 5500000), - (2, 'usdt', 'nln', 100), - (3, 'btc', 'usdt', 55000), - (4, 'eth', 'usdt', 3800) -ON CONFLICT DO NOTHING; - --- Test currency rate -INSERT INTO currency_rate(id, source_currency, dest_currency, rate) -VALUES (5, 'tbtc', 'nln', 5500000), - (6, 'tusdt', 'nln', 100), - (7, 'tbtc', 'tusdt', 55000), - (8, 'teth', 'tusdt', 3800) -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('currency_rate', 'id'), (SELECT MAX(id) FROM currency_rate)); - -INSERT INTO wallet(id, owner, wallet_type, currency, balance) -VALUES (1, 1, 'main', 'btc', 10), - (2, 1, 'exchange', 'btc', 0), - (3, 1, 'main', 'usdt', 550000), - (4, 1, 'exchange', 'usdt', 0), - (5, 1, 'main', 'nln', 100000000), - (6, 1, 'exchange', 'nln', 0), - (7, 1, 'main', 'eth', 10000), - (8, 1, 'exchange', 'eth', 0), - (9, 1, 'main', 'IRT', 100000000), - (10, 1, 'exchange', 'IRT', 0) -ON CONFLICT DO NOTHING; - --- Test wallet -INSERT INTO wallet(id, owner, wallet_type, currency, balance) -VALUES (11, 1, 'main', 'tbtc', 10), - (12, 1, 'exchange', 'tbtc', 0), - (13, 1, 'main', 'tusdt', 550000), - (14, 1, 'exchange', 'tusdt', 0), - (17, 1, 'main', 'teth', 10000), - (18, 1, 'exchange', 'teth', 0) -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('wallet', 'id'), (SELECT MAX(id) FROM wallet)); - -INSERT INTO user_limits(id, - level, - owner, - action, - wallet_type, - daily_total, - daily_count, - monthly_total, - monthly_count) -VALUES (1, null, 1, 'withdraw', 'main', 1000, 100, 10000, 1000) -ON CONFLICT DO NOTHING; - -SELECT setval(pg_get_serial_sequence('user_limits', 'id'), (SELECT MAX(id) FROM user_limits)); diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql index fd9462799..78e30bd31 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql @@ -5,15 +5,6 @@ CREATE TABLE IF NOT EXISTS currency precision DECIMAL NOT NULL ); -CREATE TABLE IF NOT EXISTS currency_rate -( - id SERIAL PRIMARY KEY, - source_currency VARCHAR(25) NOT NULL REFERENCES currency (symbol), - dest_currency VARCHAR(25) NOT NULL REFERENCES currency (symbol), - rate DECIMAL NOT NULL, - UNIQUE (source_currency, dest_currency) -); - CREATE TABLE IF NOT EXISTS wallet_owner ( id SERIAL PRIMARY KEY, @@ -31,7 +22,8 @@ CREATE TABLE IF NOT EXISTS wallet owner INTEGER NOT NULL REFERENCES wallet_owner (id), wallet_type VARCHAR(10) NOT NULL, currency VARCHAR(25) NOT NULL REFERENCES currency (symbol), - balance DECIMAL NOT NULL + balance DECIMAL NOT NULL, + UNIQUE (owner, wallet_type, currency) ); CREATE TABLE IF NOT EXISTS transaction diff --git a/websocket/pom.xml b/websocket/pom.xml index 9884fd020..447a46cd4 100644 --- a/websocket/pom.xml +++ b/websocket/pom.xml @@ -5,7 +5,7 @@ 4.0.0 - OPEX-Core + core co.nilin.opex 1.0-SNAPSHOT @@ -67,4 +67,4 @@ - \ No newline at end of file + diff --git a/websocket/websocket-app/Dockerfile b/websocket/websocket-app/Dockerfile index 7c71f9447..268392261 100644 --- a/websocket/websocket-app/Dockerfile +++ b/websocket/websocket-app/Dockerfile @@ -1,4 +1,5 @@ FROM openjdk:11 ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] +HEALTHCHECK --interval=45s --start-period=30s --retries=5 CMD curl -f 'http://localhost:8080/actuator/health' || exit 1 \ No newline at end of file diff --git a/websocket/websocket-app/src/main/resources/application.yml b/websocket/websocket-app/src/main/resources/application.yml index fddd85018..ef40309a2 100644 --- a/websocket/websocket-app/src/main/resources/application.yml +++ b/websocket/websocket-app/src/main/resources/application.yml @@ -41,5 +41,6 @@ spring: config: import: vault://secret/${spring.application.name} app: + address: 1 auth: cert-url: http://auth:8080/auth/realms/opex/protocol/openid-connect/certs \ No newline at end of file diff --git a/websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/websocket/ports/kafka/listener/config/WebSocketKafkaConfig.kt b/websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/websocket/ports/kafka/listener/config/WebSocketKafkaConfig.kt index dbf7e3f28..ad6049b18 100644 --- a/websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/websocket/ports/kafka/listener/config/WebSocketKafkaConfig.kt +++ b/websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/websocket/ports/kafka/listener/config/WebSocketKafkaConfig.kt @@ -1,98 +1,99 @@ -package co.nilin.opex.websocket.ports.kafka.listener.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import co.nilin.opex.websocket.ports.kafka.listener.consumer.OrderKafkaListener -import co.nilin.opex.websocket.ports.kafka.listener.consumer.TradeKafkaListener -import org.apache.kafka.clients.consumer.ConsumerConfig -import org.apache.kafka.common.TopicPartition -import org.apache.kafka.common.serialization.StringDeserializer -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.ConsumerFactory -import org.springframework.kafka.core.DefaultKafkaConsumerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.listener.* -import org.springframework.kafka.support.serializer.JsonDeserializer -import org.springframework.util.backoff.FixedBackOff -import java.util.regex.Pattern - -@Configuration -class WebSocketKafkaConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Value("\${spring.kafka.consumer.group-id}") - private lateinit var groupId: String - - @Bean("consumerConfigs") - fun consumerConfigs(): Map { - return mapOf( - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ConsumerConfig.GROUP_ID_CONFIG to groupId, - ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, - JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*", - ) - } - - @Bean("eventConsumerFactory") - fun consumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Bean("richTradeConsumerFactory") - fun richTradeConsumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Bean("richOrderConsumerFactory") - fun richOrderConsumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Autowired - @ConditionalOnBean(TradeKafkaListener::class) - fun configureTradeListener( - tradeListener: TradeKafkaListener, - template: KafkaTemplate, - @Qualifier("richTradeConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("richTrade")) - containerProps.messageListener = tradeListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("WebsocketTradeKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "richTrade.DLT") - container.start() - } - - @Autowired - @ConditionalOnBean(OrderKafkaListener::class) - fun configureOrderListener( - orderListener: OrderKafkaListener, - template: KafkaTemplate, - @Qualifier("richOrderConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("richOrder")) - containerProps.messageListener = orderListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("WebsocketOrderKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "richOrder.DLT") - container.start() - } - - private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler { - val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ -> - cr.headers().add("dlt-origin-module", "WEBSOCKET".toByteArray()) - TopicPartition(dltTopic, cr.partition()) - } - return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) - } - +package co.nilin.opex.websocket.ports.kafka.listener.config + +import co.nilin.opex.accountant.core.inout.RichOrderEvent +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent +import co.nilin.opex.websocket.ports.kafka.listener.consumer.OrderKafkaListener +import co.nilin.opex.websocket.ports.kafka.listener.consumer.TradeKafkaListener +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.TopicPartition +import org.apache.kafka.common.serialization.StringDeserializer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.* +import org.springframework.kafka.support.serializer.JsonDeserializer +import org.springframework.util.backoff.FixedBackOff +import java.util.regex.Pattern + +@Configuration +class WebSocketKafkaConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Value("\${spring.kafka.consumer.group-id}") + private lateinit var groupId: String + + @Bean("consumerConfigs") + fun consumerConfigs(): Map { + return mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ConsumerConfig.GROUP_ID_CONFIG to groupId, + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, + JsonDeserializer.TYPE_MAPPINGS to "rich_order_event:co.nilin.opex.accountant.core.inout.RichOrderEvent,rich_order:co.nilin.opex.accountant.core.inout.RichOrder,rich_order_update:co.nilin.opex.accountant.core.inout.RichOrderUpdate, rich_trade:co.nilin.opex.accountant.core.inout.RichTrade", + JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*", + ) + } + + @Bean("eventConsumerFactory") + fun consumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Bean("richTradeConsumerFactory") + fun richTradeConsumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Bean("richOrderConsumerFactory") + fun richOrderConsumerFactory(@Qualifier("consumerConfigs") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Autowired + @ConditionalOnBean(TradeKafkaListener::class) + fun configureTradeListener( + tradeListener: TradeKafkaListener, + template: KafkaTemplate, + @Qualifier("richTradeConsumerFactory") consumerFactory: ConsumerFactory + ) { + val containerProps = ContainerProperties(Pattern.compile("richTrade")) + containerProps.messageListener = tradeListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.setBeanName("WebsocketTradeKafkaListenerContainer") + container.commonErrorHandler = createConsumerErrorHandler(template, "richTrade.DLT") + container.start() + } + + @Autowired + @ConditionalOnBean(OrderKafkaListener::class) + fun configureOrderListener( + orderListener: OrderKafkaListener, + template: KafkaTemplate, + @Qualifier("richOrderConsumerFactory") consumerFactory: ConsumerFactory + ) { + val containerProps = ContainerProperties(Pattern.compile("richOrder")) + containerProps.messageListener = orderListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.setBeanName("WebsocketOrderKafkaListenerContainer") + container.commonErrorHandler = createConsumerErrorHandler(template, "richOrder.DLT") + container.start() + } + + private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler { + val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ -> + cr.headers().add("dlt-origin-module", "WEBSOCKET".toByteArray()) + TopicPartition(dltTopic, cr.partition()) + } + return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) + } + } \ No newline at end of file