From 0f624ad8628e9b754ac4d2e73f71bd9c660649b8 Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 10 Nov 2021 18:12:29 +0330 Subject: [PATCH 1/8] Starting websocket --- Api/api-app/pom.xml | 5 + Api/api-ports/api-websocket/.gitignore | 33 ++ Api/api-ports/api-websocket/mvnw | 310 ++++++++++++++++++ Api/api-ports/api-websocket/mvnw.cmd | 182 ++++++++++ Api/api-ports/api-websocket/pom.xml | 122 +++++++ .../api/websocket/config/WebSocketConfig.kt | 63 ++++ .../websocket/WebsocketApplicationTests.kt | 13 + Api/pom.xml | 1 + 8 files changed, 729 insertions(+) create mode 100644 Api/api-ports/api-websocket/.gitignore create mode 100644 Api/api-ports/api-websocket/mvnw create mode 100644 Api/api-ports/api-websocket/mvnw.cmd create mode 100644 Api/api-ports/api-websocket/pom.xml create mode 100644 Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt create mode 100644 Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt diff --git a/Api/api-app/pom.xml b/Api/api-app/pom.xml index f430eb9c5..2aba37703 100644 --- a/Api/api-app/pom.xml +++ b/Api/api-app/pom.xml @@ -81,6 +81,11 @@ api-persister-postgres ${api.version} + + co.nilin.opex + api-websocket + ${api.version} + io.springfox springfox-boot-starter diff --git a/Api/api-ports/api-websocket/.gitignore b/Api/api-ports/api-websocket/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/Api/api-ports/api-websocket/.gitignore @@ -0,0 +1,33 @@ +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 + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Api/api-ports/api-websocket/mvnw b/Api/api-ports/api-websocket/mvnw new file mode 100644 index 000000000..a16b5431b --- /dev/null +++ b/Api/api-ports/api-websocket/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/Api/api-ports/api-websocket/mvnw.cmd b/Api/api-ports/api-websocket/mvnw.cmd new file mode 100644 index 000000000..c8d43372c --- /dev/null +++ b/Api/api-ports/api-websocket/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/Api/api-ports/api-websocket/pom.xml b/Api/api-ports/api-websocket/pom.xml new file mode 100644 index 000000000..b929e599a --- /dev/null +++ b/Api/api-ports/api-websocket/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.4 + + + co.nilin.opex + api-websocket + 1.0-SNAPSHOT + api-websocket + Websocket handler + + + 1.8 + 1.4.31 + ${version} + ${version} + ${version} + ${version} + + + + + co.nilin.opex + matching-core + ${matching.version} + provided + + + co.nilin.opex + api-core + ${api.version} + provided + + + co.nilin.opex + accountant-core + ${accountant.version} + provided + + + co.nilin.opex + error-handler + ${utility.version} + provided + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-websocket + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt new file mode 100644 index 000000000..327efddc1 --- /dev/null +++ b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt @@ -0,0 +1,63 @@ +package co.nilin.opex.port.api.websocket.config + +import org.slf4j.LoggerFactory +import org.springframework.context.ApplicationListener +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.messaging.* + +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfig : WebSocketMessageBrokerConfigurer { + + private val logger = LoggerFactory.getLogger(WebSocketConfig::class.java) + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/socket") + .setAllowedOriginPatterns("*") + .withSockJS() + } + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + with(registry) { + enableSimpleBroker("/topic/", "/queue/") + setApplicationDestinationPrefixes("/app") + } + } + + @Bean + fun brokerAvailabilityListener() = ApplicationListener { event -> + println("Is broker available: ${event.isBrokerAvailable}") + } + + @Bean + fun sessionConnectListener() = ApplicationListener { event -> + println("* session connect received: ${event.message}") + } + + @Bean + fun sessionConnectedListener() = ApplicationListener { event -> + println("* connected: ${event.message}") + } + + @Bean + fun sessionDisconnectedListener() = ApplicationListener { event -> + println("* disconnected: ${event.message}") + } + + @Bean + fun sessionSubscribeListener() = ApplicationListener { event -> + println("+ subscribed: ${event.message}") + } + + @Bean + fun sessionUnsubscribeEventListener() = ApplicationListener { event -> + println("- unsubscribed: ${event.message}") + } + +} \ No newline at end of file diff --git a/Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt b/Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt new file mode 100644 index 000000000..fa05be823 --- /dev/null +++ b/Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.port.api.websocket + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class WebsocketApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/Api/pom.xml b/Api/pom.xml index 8e670c159..779f877e2 100644 --- a/Api/pom.xml +++ b/Api/pom.xml @@ -14,5 +14,6 @@ api-ports/api-persister-postgres api-ports/api-binance-rest api-ports/api-eventlistener-kafka + api-ports/api-websocket From 1e092e909bcf70a0948b84410ea1c19cb351c62d Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 14 Nov 2021 12:22:49 +0330 Subject: [PATCH 2/8] Add security config --- .../port/api/binance/config/SecurityConfig.kt | 1 + Api/api-ports/api-websocket/pom.xml | 17 +++++--- .../api/websocket/config/AuthInterceptor.kt | 42 +++++++++++++++++++ .../config/WebSocketAuthenticationConfig.kt | 21 ++++++++++ .../config/WebSocketAuthorizationConfig.kt | 21 ++++++++++ .../api/websocket/config/WebSocketConfig.kt | 5 ++- .../controller/WebSocketController.kt | 15 +++++++ .../websocket/WebsocketApplicationTests.kt | 13 ------ 8 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt create mode 100644 Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt create mode 100644 Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt create mode 100644 Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt delete mode 100644 Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt diff --git a/Api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/port/api/binance/config/SecurityConfig.kt b/Api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/port/api/binance/config/SecurityConfig.kt index 12b98b829..047c58dc5 100644 --- a/Api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/port/api/binance/config/SecurityConfig.kt +++ b/Api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/port/api/binance/config/SecurityConfig.kt @@ -30,6 +30,7 @@ class SecurityConfig(private val webClient: WebClient) { .pathMatchers("/v3/ticker/**").permitAll() .pathMatchers("/v3/exchangeInfo").permitAll() .pathMatchers("/v3/klines").permitAll() + .pathMatchers("/socket").permitAll() .pathMatchers("/**").hasAuthority("SCOPE_trust") .anyExchange().authenticated() .and() diff --git a/Api/api-ports/api-websocket/pom.xml b/Api/api-ports/api-websocket/pom.xml index b929e599a..37e2fd68f 100644 --- a/Api/api-ports/api-websocket/pom.xml +++ b/Api/api-ports/api-websocket/pom.xml @@ -52,10 +52,22 @@ org.springframework.boot spring-boot-starter-webflux + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.boot spring-boot-starter-websocket + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-messaging + com.fasterxml.jackson.module jackson-module-kotlin @@ -76,7 +88,6 @@ org.jetbrains.kotlinx kotlinx-coroutines-reactor - org.springframework.boot spring-boot-starter-test @@ -93,10 +104,6 @@ ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin - - org.springframework.boot - spring-boot-maven-plugin - org.jetbrains.kotlin kotlin-maven-plugin diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt new file mode 100644 index 000000000..3ebdcc2e0 --- /dev/null +++ b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt @@ -0,0 +1,42 @@ +package co.nilin.opex.port.api.websocket.config + +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.simp.stomp.StompCommand +import org.springframework.messaging.simp.stomp.StompHeaderAccessor +import org.springframework.messaging.support.ChannelInterceptor +import org.springframework.messaging.support.MessageHeaderAccessor +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter +import org.springframework.stereotype.Component + +@Component +class AuthInterceptor : ChannelInterceptor { + + private val logger = LoggerFactory.getLogger(ChannelInterceptor::class.java) + + @Autowired + private lateinit var jwtDecoder: ReactiveJwtDecoder + private val converter = JwtAuthenticationConverter() + + override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> { + val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java) + if (accessor?.command == StompCommand.CONNECT) { + val authorization = accessor.getNativeHeader("X-Authorization") + logger.debug("Authorization: $authorization") + + if (authorization.isNullOrEmpty()) + return message + + val token = authorization[0].split(" ")[1] + + val jwt = jwtDecoder.decode(token) + val auth = converter.convert(jwt.block()!!) + accessor.user = auth + } + return message + } + +} \ No newline at end of file diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt new file mode 100644 index 000000000..03f5c1b92 --- /dev/null +++ b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.port.api.websocket.config + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.messaging.simp.config.ChannelRegistration +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +@Configuration +@Order(Ordered.HIGHEST_PRECEDENCE + 99) +class WebSocketAuthenticationConfig : WebSocketMessageBrokerConfigurer { + + @Autowired + private lateinit var authInterceptor: AuthInterceptor + + override fun configureClientInboundChannel(registration: ChannelRegistration) { + registration.interceptors(authInterceptor) + } + +} \ No newline at end of file diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt new file mode 100644 index 000000000..47bc2fa15 --- /dev/null +++ b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.port.api.websocket.config + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry +import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer + +@Configuration +class WebSocketAuthorizationConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { + + override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { + with(messages) { + simpDestMatchers("/secured/**").hasAuthority("SCOPE_trust") + anyMessage().permitAll() + } + } + + override fun sameOriginDisabled(): Boolean { + return true + } + +} \ No newline at end of file diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt index 327efddc1..09fcd5296 100644 --- a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt +++ b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt @@ -25,8 +25,9 @@ class WebSocketConfig : WebSocketMessageBrokerConfigurer { override fun configureMessageBroker(registry: MessageBrokerRegistry) { with(registry) { - enableSimpleBroker("/topic/", "/queue/") - setApplicationDestinationPrefixes("/app") + enableSimpleBroker("/topic", "/secured/user/queue") + setApplicationDestinationPrefixes("/secured/app") + setUserDestinationPrefix("/secured/user") } } diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt new file mode 100644 index 000000000..70bcd1688 --- /dev/null +++ b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.port.api.websocket.controller + +import org.springframework.messaging.simp.annotation.SubscribeMapping +import org.springframework.stereotype.Controller + +@Controller +class WebSocketController { + + @SubscribeMapping("/test") + fun test(): String { + print("") + return "" + } + +} \ No newline at end of file diff --git a/Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt b/Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt deleted file mode 100644 index fa05be823..000000000 --- a/Api/api-ports/api-websocket/src/test/kotlin/co/nilin/opex/port/api/websocket/WebsocketApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package co.nilin.opex.port.api.websocket - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class WebsocketApplicationTests { - - @Test - fun contextLoads() { - } - -} From 86ad9637d85e72982ab8e05df0dc63ac8a86c007 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 16 Nov 2021 13:50:27 +0330 Subject: [PATCH 3/8] Created websocket modules --- Api/api-app/pom.xml | 7 +- .../main/kotlin/co/nilin/opex/app/ApiApp.kt | 14 - Api/api-ports/api-websocket/mvnw | 310 ------------------ Api/api-ports/api-websocket/mvnw.cmd | 182 ---------- .../controller/WebSocketController.kt | 15 - Api/pom.xml | 1 - Deployment/docker-compose.yml | 48 ++- Websocket/pom.xml | 18 + .../websocket-app}/.gitignore | 0 Websocket/websocket-app/Dockerfile | 5 + Websocket/websocket-app/pom.xml | 230 +++++++++++++ .../nilin/opex/port/websocket/WebSocketApp.kt | 15 + .../opex/port/websocket/config/AppConfig.kt | 28 ++ .../port/websocket/config/AppDispatchers.kt | 11 + .../websocket/config/WebSecurityConfig.kt | 41 +++ .../websocket/controller/MarketController.kt | 15 + .../controller/WebSocketController.kt | 30 ++ .../listener/WebSocketKafkaListener.kt | 37 +++ .../port/websocket/service/SubscriberPool.kt | 39 +++ .../port/websocket/socket}/AuthInterceptor.kt | 10 +- .../socket}/WebSocketAuthenticationConfig.kt | 2 +- .../socket}/WebSocketAuthorizationConfig.kt | 2 +- .../port/websocket/socket}/WebSocketConfig.kt | 10 +- .../src/main/resources/application-docker.yml | 15 + .../src/main/resources/application.yml | 28 ++ Websocket/websocket-core/.gitignore | 4 + Websocket/websocket-core/pom.xml | 104 ++++++ .../core/inout/AggregatedOrderPriceModel.kt | 6 + .../websocket/core/inout/AllOrderRequest.kt | 10 + .../opex/websocket/core/inout/CandleData.kt | 17 + .../core/inout/MarketTradeResponse.kt | 15 + .../websocket/core/inout/OrderBookResponse.kt | 8 + .../opex/websocket/core/inout/OrderEnums.kt | 41 +++ .../core/inout/PriceChangeResponse.kt | 21 ++ .../core/inout/PriceTickerResponse.kt | 6 + .../websocket/core/inout/QueryOrderRequest.kt | 7 + .../core/inout/QueryOrderResponse.kt | 26 ++ .../opex/websocket/core/inout/TradeRequest.kt | 11 + .../websocket/core/inout/TradeResponse.kt | 21 ++ .../websocket/core/spi/MarketQueryHandler.kt | 29 ++ .../opex/websocket/core/spi/SymbolMapper.kt | 10 + .../websocket/core/spi/UserQueryHandler.kt | 16 + Websocket/websocket-ports/.gitignore | 33 ++ .../websocket-eventlistener-kafka/.gitignore | 34 ++ .../websocket-eventlistener-kafka/pom.xml | 119 +++++++ .../kafka/config/WebSocketKafkaConfig.kt | 108 ++++++ .../kafka/consumer/EventKafkaListener.kt | 30 ++ .../kafka/consumer/OrderKafkaListener.kt | 30 ++ .../kafka/consumer/TradeKafkaListener.kt | 29 ++ .../port/websocket/kafka/spi/EventListener.kt | 9 + .../websocket/kafka/spi/RichOrderListener.kt | 8 + .../websocket/kafka/spi/RichTradeListener.kt | 8 + .../websocket-persister-postgres/.gitignore | 34 ++ .../websocket-persister-postgres}/pom.xml | 55 ++-- .../postgres/config/PostgresConfig.kt | 8 + .../websocket/postgres/dao/OrderRepository.kt | 106 ++++++ .../postgres/dao/SymbolMapRepository.kt | 18 + .../websocket/postgres/dao/TradeRepository.kt | 150 +++++++++ .../postgres/impl/MarketQueryHandlerImpl.kt | 209 ++++++++++++ .../postgres/impl/SymbolMapperImpl.kt | 30 ++ .../postgres/impl/UserQueryHandlerImpl.kt | 139 ++++++++ .../postgres/model/CandleInfoData.kt | 17 + .../websocket/postgres/model/OrderModel.kt | 42 +++ .../postgres/model/SymbolMapModel.kt | 12 + .../websocket/postgres/model/TradeModel.kt | 27 ++ .../postgres/model/TradeTickerData.kt | 33 ++ .../websocket/postgres/util/EnumExtensions.kt | 45 +++ Websocket/websocket-root.iml | 12 + 68 files changed, 2231 insertions(+), 579 deletions(-) delete mode 100644 Api/api-ports/api-websocket/mvnw delete mode 100644 Api/api-ports/api-websocket/mvnw.cmd delete mode 100644 Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt create mode 100644 Websocket/pom.xml rename {Api/api-ports/api-websocket => Websocket/websocket-app}/.gitignore (100%) create mode 100644 Websocket/websocket-app/Dockerfile create mode 100644 Websocket/websocket-app/pom.xml create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/WebSocketApp.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt rename {Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config => Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket}/AuthInterceptor.kt (83%) rename {Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config => Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket}/WebSocketAuthenticationConfig.kt (93%) rename {Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config => Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket}/WebSocketAuthorizationConfig.kt (93%) rename {Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config => Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket}/WebSocketConfig.kt (88%) create mode 100644 Websocket/websocket-app/src/main/resources/application-docker.yml create mode 100644 Websocket/websocket-app/src/main/resources/application.yml create mode 100644 Websocket/websocket-core/.gitignore create mode 100644 Websocket/websocket-core/pom.xml create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AggregatedOrderPriceModel.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AllOrderRequest.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/CandleData.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/MarketTradeResponse.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderBookResponse.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderEnums.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceChangeResponse.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceTickerResponse.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderRequest.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderResponse.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeRequest.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeResponse.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/MarketQueryHandler.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/SymbolMapper.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/UserQueryHandler.kt create mode 100644 Websocket/websocket-ports/.gitignore create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/.gitignore create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/pom.xml create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/TradeKafkaListener.kt create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/EventListener.kt create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichOrderListener.kt create mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichTradeListener.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/.gitignore rename {Api/api-ports/api-websocket => Websocket/websocket-ports/websocket-persister-postgres}/pom.xml (74%) create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/config/PostgresConfig.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/OrderRepository.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/SymbolMapRepository.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/TradeRepository.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/MarketQueryHandlerImpl.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/SymbolMapperImpl.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/UserQueryHandlerImpl.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/CandleInfoData.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/OrderModel.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/SymbolMapModel.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeModel.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeTickerData.kt create mode 100644 Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/util/EnumExtensions.kt create mode 100644 Websocket/websocket-root.iml diff --git a/Api/api-app/pom.xml b/Api/api-app/pom.xml index 2aba37703..620cef3b9 100644 --- a/Api/api-app/pom.xml +++ b/Api/api-app/pom.xml @@ -81,11 +81,6 @@ api-persister-postgres ${api.version} - - co.nilin.opex - api-websocket - ${api.version} - io.springfox springfox-boot-starter @@ -213,6 +208,6 @@ - opex-accountant + opex-api diff --git a/Api/api-app/src/main/kotlin/co/nilin/opex/app/ApiApp.kt b/Api/api-app/src/main/kotlin/co/nilin/opex/app/ApiApp.kt index 338e2bab1..841b43d18 100644 --- a/Api/api-app/src/main/kotlin/co/nilin/opex/app/ApiApp.kt +++ b/Api/api-app/src/main/kotlin/co/nilin/opex/app/ApiApp.kt @@ -1,23 +1,9 @@ package co.nilin.opex.app -import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan -import org.springframework.security.core.annotation.AuthenticationPrincipal -import springfox.documentation.builders.* -import springfox.documentation.builders.PathSelectors.regex -import springfox.documentation.service.* -import springfox.documentation.spi.DocumentationType -import springfox.documentation.spi.service.contexts.SecurityContext -import springfox.documentation.spring.web.plugins.Docket -import springfox.documentation.swagger.web.SecurityConfiguration -import springfox.documentation.swagger.web.SecurityConfigurationBuilder import springfox.documentation.swagger2.annotations.EnableSwagger2 -import java.security.Principal -import java.util.Collections.singletonList - @SpringBootApplication @ComponentScan("co.nilin.opex") diff --git a/Api/api-ports/api-websocket/mvnw b/Api/api-ports/api-websocket/mvnw deleted file mode 100644 index a16b5431b..000000000 --- a/Api/api-ports/api-websocket/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/Api/api-ports/api-websocket/mvnw.cmd b/Api/api-ports/api-websocket/mvnw.cmd deleted file mode 100644 index c8d43372c..000000000 --- a/Api/api-ports/api-websocket/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt b/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt deleted file mode 100644 index 70bcd1688..000000000 --- a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/controller/WebSocketController.kt +++ /dev/null @@ -1,15 +0,0 @@ -package co.nilin.opex.port.api.websocket.controller - -import org.springframework.messaging.simp.annotation.SubscribeMapping -import org.springframework.stereotype.Controller - -@Controller -class WebSocketController { - - @SubscribeMapping("/test") - fun test(): String { - print("") - return "" - } - -} \ No newline at end of file diff --git a/Api/pom.xml b/Api/pom.xml index 779f877e2..8e670c159 100644 --- a/Api/pom.xml +++ b/Api/pom.xml @@ -14,6 +14,5 @@ api-ports/api-persister-postgres api-ports/api-binance-rest api-ports/api-eventlistener-kafka - api-ports/api-websocket diff --git a/Deployment/docker-compose.yml b/Deployment/docker-compose.yml index 7f511f6a2..332a62ef0 100644 --- a/Deployment/docker-compose.yml +++ b/Deployment/docker-compose.yml @@ -306,21 +306,30 @@ services: deploy: restart_policy: condition: on-failure - nginx: - image: jboesl/docker-nginx-headers-more - container_name: opex_nginx - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf - - $DATA/www:/data/www + websocket: + container_name: websocket + build: + context: ../Websocket/websocket-app + dockerfile: Dockerfile ports: - - 80:80 - depends_on: - - wallet - - auth - - matching-gateway - - api + - 127.0.0.1:8097:8097 + - 127.0.0.1:1054:1044 + environment: + - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044 + - SPRING_PROFILES_DEFAULT=docker + - KAFKA_IP_PORT=kafka:9092 + - CONSUL_HOST=consul + - DB_IP_PORT=postgres-api networks: - opex + depends_on: + - zookeeper + - kafka + - consul + - postgres-api + deploy: + restart_policy: + condition: on-failure bc-gateway: container_name: bc-gateway build: @@ -366,6 +375,21 @@ services: deploy: restart_policy: condition: on-failure + nginx: + image: jboesl/docker-nginx-headers-more + container_name: opex_nginx + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - $DATA/www:/data/www + ports: + - 80:80 + depends_on: + - wallet + - auth + - matching-gateway + - api + networks: + - opex networks: opex: driver: bridge diff --git a/Websocket/pom.xml b/Websocket/pom.xml new file mode 100644 index 000000000..e619d6c40 --- /dev/null +++ b/Websocket/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + co.nilin.opex + websocket-root + 1.0-SNAPSHOT + websocket-root + pom + Websocket root module + + websocket-app + websocket-core + websocket-ports/websocket-eventlistener-kafka + websocket-ports/websocket-persister-postgres + + \ No newline at end of file diff --git a/Api/api-ports/api-websocket/.gitignore b/Websocket/websocket-app/.gitignore similarity index 100% rename from Api/api-ports/api-websocket/.gitignore rename to Websocket/websocket-app/.gitignore diff --git a/Websocket/websocket-app/Dockerfile b/Websocket/websocket-app/Dockerfile new file mode 100644 index 000000000..f2cbd4c26 --- /dev/null +++ b/Websocket/websocket-app/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:8-jdk-alpine +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 diff --git a/Websocket/websocket-app/pom.xml b/Websocket/websocket-app/pom.xml new file mode 100644 index 000000000..c0e773b5e --- /dev/null +++ b/Websocket/websocket-app/pom.xml @@ -0,0 +1,230 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.4 + + + co.nilin.opex + websocket-app + 1.0-SNAPSHOT + websocket-app + Websocket app + + + 1.8 + 1.4.31 + 2020.0.2 + ${version} + ${version} + ${version} + ${version} + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.cloud + spring-cloud-starter-consul-all + + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.security + spring-security-messaging + + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + + co.nilin.opex + accountant-core + ${accountant.version} + + + co.nilin.opex + websocket-core + ${websocket.version} + + + co.nilin.opex + websocket-eventlistener-kafka + ${websocket.version} + + + co.nilin.opex + websocket-persister-postgres + ${websocket.version} + + + + io.projectreactor + reactor-test + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18 + + + ${skip.unit.tests} + + + **/*IntegrationTest.java + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/java + + + + + compile + + add-source + + + + src/main/java + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + 1.8 + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + + opex-websocket + + + diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/WebSocketApp.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/WebSocketApp.kt new file mode 100644 index 000000000..26150f8a6 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/WebSocketApp.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.port.websocket + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan +import org.springframework.scheduling.annotation.EnableScheduling + +@SpringBootApplication +@EnableScheduling +@ComponentScan("co.nilin.opex") +class WebSocketApp + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt new file mode 100644 index 000000000..0ecdc37c2 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt @@ -0,0 +1,28 @@ +package co.nilin.opex.port.websocket.config + +import co.nilin.opex.port.websocket.kafka.consumer.OrderKafkaListener +import co.nilin.opex.port.websocket.kafka.consumer.TradeKafkaListener +import co.nilin.opex.port.websocket.listener.WebSocketKafkaListener +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class AppConfig { + + @Autowired + fun configureListeners( + orderKafkaListener: OrderKafkaListener, + tradeKafkaListener: TradeKafkaListener, + appListener: WebSocketKafkaListener + ) { + orderKafkaListener.addOrderListener(appListener) + tradeKafkaListener.addTradeListener(appListener) + } + + @Bean + fun websocketListener(): WebSocketKafkaListener { + return WebSocketKafkaListener() + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt new file mode 100644 index 000000000..071845ce1 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.port.websocket.config + +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors + +object AppDispatchers { + + val websocketExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + val kafkaExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt new file mode 100644 index 000000000..1df8decd7 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt @@ -0,0 +1,41 @@ +package co.nilin.opex.port.websocket.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder +import org.springframework.web.reactive.function.client.WebClient + +@Configuration +class WebSecurityConfig : WebSecurityConfigurerAdapter() { + + @Value("\${app.auth.cert-url}") + private lateinit var jwkUrl: String + + override fun configure(web: WebSecurity) { + web.ignoring().antMatchers("/actuator/health") + } + + override fun configure(http: HttpSecurity) { + http.httpBasic().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .authorizeRequests() + .antMatchers("/stream/**").permitAll() + .anyRequest().denyAll() + .and() + .oauth2ResourceServer() + .jwt() + } + + @Bean + @Throws(Exception::class) + fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri(jwkUrl).build() + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt new file mode 100644 index 000000000..053e433cd --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.port.websocket.controller + +import org.springframework.messaging.handler.annotation.Header +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.stereotype.Controller + +@Controller +class MarketController { + + @MessageMapping("/market/depth") + fun orderBook(@Header("symbol") symbol:String):List{ + return emptyList() + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt new file mode 100644 index 000000000..49329e4dd --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.port.websocket.controller + +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.handler.annotation.SendTo +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.messaging.simp.annotation.SubscribeMapping +import org.springframework.stereotype.Controller +import java.security.Principal + +@Controller +class WebSocketController(private val template: SimpMessagingTemplate) { + + @SubscribeMapping("/test") + fun test(): String { + print("sss") + return "sss" + } + + @MessageMapping("/a") + @SendTo("/secured/queue/a") + fun a(): String { + return "this is a" + } + + @MessageMapping("/b") + fun b(principal: Principal) { + template.convertAndSendToUser(principal.name,"/secured/queue/b","this is b") + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt new file mode 100644 index 000000000..c6494794d --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt @@ -0,0 +1,37 @@ +package co.nilin.opex.port.websocket.listener + +import co.nilin.opex.accountant.core.inout.RichOrder +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.port.websocket.config.AppDispatchers +import co.nilin.opex.port.websocket.kafka.spi.RichOrderListener +import co.nilin.opex.port.websocket.kafka.spi.RichTradeListener +import kotlinx.coroutines.runBlocking + +class WebSocketKafkaListener : RichTradeListener, RichOrderListener { + + override fun id(): String { + return "WebSocketKafkaListener" + } + + override fun onTrade( + trade: RichTrade, + partition: Int, + offset: Long, + timestamp: Long + ) { + runBlocking(AppDispatchers.kafkaExecutor) { + //TODO send to user + } + } + + override fun onOrder( + order: RichOrder, + partition: Int, + offset: Long, + timestamp: Long + ) { + runBlocking(AppDispatchers.kafkaExecutor) { + //TODO send to user + } + } +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt new file mode 100644 index 000000000..e5e1cd01f --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt @@ -0,0 +1,39 @@ +package co.nilin.opex.port.websocket.service + +import org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_SINGLETON +import org.springframework.context.annotation.Scope +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Component + +@Component +@Scope(SCOPE_SINGLETON) +class SubscriberPool(private val template: SimpMessagingTemplate) { + + private val public = hashSetOf() + private val secured = hashSetOf() + + fun sendPublicMessage(path: String, message: Any) { + if (hasPublicSubscriber()) + template.convertAndSend(path, message) + } + + fun sendSecuredMessage(path: String, user: String, message: Any) { + if (hasSecuredSubscriber()) + template.convertAndSendToUser(user, path, message) + } + + fun addPublicSubscriber(sub: String) { + public.add(sub) + } + + fun addSecuredSubscriber(sub: String) { + secured.add(sub) + } + + fun hasPublicSubscriber() = public.isNotEmpty() + + fun hasSecuredSubscriber() = secured.isNotEmpty() + + fun hasAnySubscriber() = hasPublicSubscriber() && hasSecuredSubscriber() + +} \ No newline at end of file diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt similarity index 83% rename from Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt rename to Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt index 3ebdcc2e0..abec921dd 100644 --- a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/AuthInterceptor.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt @@ -1,4 +1,4 @@ -package co.nilin.opex.port.api.websocket.config +package co.nilin.opex.port.websocket.socket import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -8,7 +8,7 @@ import org.springframework.messaging.simp.stomp.StompCommand import org.springframework.messaging.simp.stomp.StompHeaderAccessor import org.springframework.messaging.support.ChannelInterceptor import org.springframework.messaging.support.MessageHeaderAccessor -import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder +import org.springframework.security.oauth2.jwt.JwtDecoder import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter import org.springframework.stereotype.Component @@ -18,7 +18,7 @@ class AuthInterceptor : ChannelInterceptor { private val logger = LoggerFactory.getLogger(ChannelInterceptor::class.java) @Autowired - private lateinit var jwtDecoder: ReactiveJwtDecoder + private lateinit var jwtDecoder: JwtDecoder private val converter = JwtAuthenticationConverter() override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> { @@ -30,10 +30,10 @@ class AuthInterceptor : ChannelInterceptor { if (authorization.isNullOrEmpty()) return message - val token = authorization[0].split(" ")[1] + val token = authorization[0] val jwt = jwtDecoder.decode(token) - val auth = converter.convert(jwt.block()!!) + val auth = converter.convert(jwt) accessor.user = auth } return message diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketAuthenticationConfig.kt similarity index 93% rename from Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt rename to Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketAuthenticationConfig.kt index 03f5c1b92..c73916e40 100644 --- a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthenticationConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketAuthenticationConfig.kt @@ -1,4 +1,4 @@ -package co.nilin.opex.port.api.websocket.config +package co.nilin.opex.port.websocket.socket import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Configuration diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketAuthorizationConfig.kt similarity index 93% rename from Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt rename to Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketAuthorizationConfig.kt index 47bc2fa15..304a2b58f 100644 --- a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketAuthorizationConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketAuthorizationConfig.kt @@ -1,4 +1,4 @@ -package co.nilin.opex.port.api.websocket.config +package co.nilin.opex.port.websocket.socket import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry diff --git a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt similarity index 88% rename from Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt rename to Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt index 09fcd5296..4d0854141 100644 --- a/Api/api-ports/api-websocket/src/main/kotlin/co/nilin/opex/port/api/websocket/config/WebSocketConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt @@ -1,4 +1,4 @@ -package co.nilin.opex.port.api.websocket.config +package co.nilin.opex.port.websocket.socket import org.slf4j.LoggerFactory import org.springframework.context.ApplicationListener @@ -18,16 +18,16 @@ class WebSocketConfig : WebSocketMessageBrokerConfigurer { private val logger = LoggerFactory.getLogger(WebSocketConfig::class.java) override fun registerStompEndpoints(registry: StompEndpointRegistry) { - registry.addEndpoint("/socket") + registry.addEndpoint("/stream") .setAllowedOriginPatterns("*") .withSockJS() } override fun configureMessageBroker(registry: MessageBrokerRegistry) { with(registry) { - enableSimpleBroker("/topic", "/secured/user/queue") - setApplicationDestinationPrefixes("/secured/app") - setUserDestinationPrefix("/secured/user") + enableSimpleBroker("/topic", "/secured/queue") + setApplicationDestinationPrefixes("/app") + //setUserDestinationPrefix("/secured/user") } } diff --git a/Websocket/websocket-app/src/main/resources/application-docker.yml b/Websocket/websocket-app/src/main/resources/application-docker.yml new file mode 100644 index 000000000..2e088ab93 --- /dev/null +++ b/Websocket/websocket-app/src/main/resources/application-docker.yml @@ -0,0 +1,15 @@ +spring: + kafka: + bootstrap-servers: ${KAFKA_IP_PORT} + redis: + host: ${REDIS_HOST} + r2dbc: + url: r2dbc:postgresql://${DB_IP_PORT}/opex_api + username: opex + password: hiopex + cloud: + consul: + host: ${CONSUL_HOST} + port: 8500 + main: + allow-bean-definition-overriding: true \ 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 new file mode 100644 index 000000000..e7df088d6 --- /dev/null +++ b/Websocket/websocket-app/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 8097 +spring: + application: + name: opex-websocket + main: + allow-bean-definition-overriding: false + kafka: + bootstrap-servers: 192.168.178.29:9092 + consumer: + group-id: websocket + r2dbc: + url: r2dbc:postgresql://localhost/opex_api + username: opex + password: hiopex + initialization-mode: always + cloud: + bootstrap: + enabled: true + consul: + port: 8500 + discovery: + instance-id: ${spring.application.name}:${server.port} + healthCheckInterval: 20s + prefer-ip-address: true +app: + auth: + cert-url: http://localhost:8083/auth/realms/opex/protocol/openid-connect/certs \ No newline at end of file diff --git a/Websocket/websocket-core/.gitignore b/Websocket/websocket-core/.gitignore new file mode 100644 index 000000000..f3a8317d6 --- /dev/null +++ b/Websocket/websocket-core/.gitignore @@ -0,0 +1,4 @@ +*.iml +target/ +.mvn/ +.idea/ \ No newline at end of file diff --git a/Websocket/websocket-core/pom.xml b/Websocket/websocket-core/pom.xml new file mode 100644 index 000000000..fe5f9c8ee --- /dev/null +++ b/Websocket/websocket-core/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.4 + + + co.nilin.opex + websocket-core + 1.0-SNAPSHOT + websocket-core + + + 1.8 + 1.4.31 + ${version} + ${version} + + + + + org.springframework.boot + spring-boot-starter + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + co.nilin.opex + matching-core + ${matching.version} + + + co.nilin.opex + accountant-core + ${accountant.version} + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework + spring-tx + provided + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AggregatedOrderPriceModel.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AggregatedOrderPriceModel.kt new file mode 100644 index 000000000..adb2ec145 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AggregatedOrderPriceModel.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.websocket.core.inout + +data class AggregatedOrderPriceModel( + val price: Double?, + val quantity: Double? +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AllOrderRequest.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AllOrderRequest.kt new file mode 100644 index 000000000..94087e701 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/AllOrderRequest.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.websocket.core.inout + +import java.util.* + +class AllOrderRequest( + val symbol: String?, + val startTime: Date?, + val endTime: Date?, + val limit: Int? = 500, //Default 500; max 1000. +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/CandleData.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/CandleData.kt new file mode 100644 index 000000000..a1dbf8ee6 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/CandleData.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.websocket.core.inout + +import java.time.LocalDateTime + +data class CandleData( + val openTime: LocalDateTime, + val closeTime: LocalDateTime, + val open: Double, + val close: Double, + val high: Double, + val low: Double, + val volume: Double, + val quoteAssetVolume: Double, + val trades: Int, + val takerBuyBaseAssetVolume: Double, + val takerBuyQuoteAssetVolume: Double, +) diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/MarketTradeResponse.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/MarketTradeResponse.kt new file mode 100644 index 000000000..e02c464f3 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/MarketTradeResponse.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.websocket.core.inout + +import java.math.BigDecimal +import java.util.* + +data class MarketTradeResponse( + val symbol: String, + val id: Long, + val price: BigDecimal, + val qty: BigDecimal, + val quoteQty: BigDecimal, + val time: Date, + val isBestMatch: Boolean, + val isMakerBuyer: Boolean +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderBookResponse.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderBookResponse.kt new file mode 100644 index 000000000..f3162f29a --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderBookResponse.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.websocket.core.inout + +import java.math.BigDecimal + +data class OrderBookResponse( + val price: BigDecimal?, + val quantity: BigDecimal? +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderEnums.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderEnums.kt new file mode 100644 index 000000000..9e986f885 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/OrderEnums.kt @@ -0,0 +1,41 @@ +package co.nilin.opex.websocket.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 +} \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceChangeResponse.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceChangeResponse.kt new file mode 100644 index 000000000..ca7592895 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceChangeResponse.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.websocket.core.inout + +data class PriceChangeResponse( + val symbol: String, + val priceChange: Double = 0.0, + val priceChangePercent: Double = 0.0, + val weightedAvgPrice: Double = 0.0, + val lastPrice: Double = 0.0, + val lastQty: Double = 0.0, + val bidPrice: Double = 0.0, + val askPrice: Double = 0.0, + val openPrice: Double = 0.0, + val highPrice: Double = 0.0, + val lowPrice: Double = 0.0, + val volume: Double = 0.0, + val openTime: Long, + val closeTime: Long, + val firstId: Long = 0, + val lastId: Long = 0, + val count: Long = 0, +) diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceTickerResponse.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceTickerResponse.kt new file mode 100644 index 000000000..1cb639b76 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/PriceTickerResponse.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.websocket.core.inout + +data class PriceTickerResponse( + val symbol: String?, + val price: String? +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderRequest.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderRequest.kt new file mode 100644 index 000000000..434056408 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderRequest.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.websocket.core.inout + +data class QueryOrderRequest( + val symbol: String, + val orderId: Long?, + val origClientOrderId: String? +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderResponse.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderResponse.kt new file mode 100644 index 000000000..d8c7b8d3b --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/QueryOrderResponse.kt @@ -0,0 +1,26 @@ +package co.nilin.opex.websocket.core.inout + +import java.math.BigDecimal +import java.util.* + +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, + val price: BigDecimal, + val origQty: BigDecimal, + val executedQty: BigDecimal, + val cummulativeQuoteQty: BigDecimal, + val status: OrderStatus, + val timeInForce: TimeInForce, + val type: OrderType, + val side: OrderSide, + val stopPrice: BigDecimal?, + val icebergQty: BigDecimal?, + val time: Date, + val updateTime: Date, + val isWorking: Boolean, + val origQuoteOrderQty: BigDecimal +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeRequest.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeRequest.kt new file mode 100644 index 000000000..27957150c --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeRequest.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.websocket.core.inout + +import java.util.* + +class TradeRequest( + val symbol: String?, + val fromTrade: Long?, + val startTime: Date?, + val endTime: Date?, + val limit: Int? = 500 //Default 500; max 1000. +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeResponse.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeResponse.kt new file mode 100644 index 000000000..935416938 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/inout/TradeResponse.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.websocket.core.inout + +import java.math.BigDecimal +import java.util.* + +data class TradeResponse( + val symbol: String, + val id: Long, + val orderId: Long, + val orderListId: Long = -1, + val price: BigDecimal, + val qty: BigDecimal, + val quoteQty: BigDecimal, + val commission: BigDecimal, + val commissionAsset: String, + val time: Date, + val isBuyer: Boolean, + val isMaker: Boolean, + val isBestMatch: Boolean, + val isMakerBuyer: Boolean +) \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/MarketQueryHandler.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/MarketQueryHandler.kt new file mode 100644 index 000000000..4676a4379 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/MarketQueryHandler.kt @@ -0,0 +1,29 @@ +package co.nilin.opex.websocket.core.spi + +import co.nilin.opex.websocket.core.inout.* +import kotlinx.coroutines.flow.Flow +import java.time.LocalDateTime + +interface MarketQueryHandler { + + suspend fun getTradeTickerDataBySymbol(symbol: String, startFrom: LocalDateTime): PriceChangeResponse + + suspend fun openBidOrders(symbol: String, limit: Int): List + + suspend fun openAskOrders(symbol: String, limit: Int): List + + suspend fun lastOrder(symbol: String): QueryOrderResponse? + + suspend fun recentTrades(symbol: String, limit: Int): Flow + + suspend fun lastPrice(symbol: String?): List + + suspend fun getCandleInfo( + symbol: String, + interval: String, + startTime: Long?, + endTime: Long?, + limit: Int + ): List + +} \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/SymbolMapper.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/SymbolMapper.kt new file mode 100644 index 000000000..76913d2e0 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/SymbolMapper.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.websocket.core.spi + +interface SymbolMapper { + + suspend fun map(symbol: String?): String? + + suspend fun unmap(value: String?): String? + + suspend fun getKeyValues(): Map +} \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/UserQueryHandler.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/UserQueryHandler.kt new file mode 100644 index 000000000..45263588a --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/UserQueryHandler.kt @@ -0,0 +1,16 @@ +package co.nilin.opex.websocket.core.spi + +import co.nilin.opex.websocket.core.inout.* +import kotlinx.coroutines.flow.Flow +import java.security.Principal + +interface UserQueryHandler { + + suspend fun queryOrder(principal: Principal, request: QueryOrderRequest): QueryOrderResponse? + + suspend fun openOrders(principal: Principal, symbol: String?): Flow + + suspend fun allOrders(principal: Principal, allOrderRequest: AllOrderRequest): Flow + + suspend fun allTrades(principal: Principal, request: TradeRequest): Flow +} \ No newline at end of file diff --git a/Websocket/websocket-ports/.gitignore b/Websocket/websocket-ports/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/Websocket/websocket-ports/.gitignore @@ -0,0 +1,33 @@ +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 + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/.gitignore b/Websocket/websocket-ports/websocket-eventlistener-kafka/.gitignore new file mode 100644 index 000000000..bb9840a17 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +.mvn/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + + +### VS Code ### +.vscode/ + +.DS_Store diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/pom.xml b/Websocket/websocket-ports/websocket-eventlistener-kafka/pom.xml new file mode 100644 index 000000000..2a2d9f3c4 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.4 + + + co.nilin.opex + websocket-eventlistener-kafka + 1.0-SNAPSHOT + websocket-eventlistener-kafka + + + 1.8 + 1.4.31 + ${version} + ${version} + ${version} + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-webflux + + + co.nilin.opex + matching-core + ${matching.version} + provided + + + co.nilin.opex + accountant-core + ${accountant.version} + provided + + + co.nilin.opex + websocket-core + ${websocket.version} + provided + + + org.springframework.kafka + spring-kafka + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + org.springframework.kafka + spring-kafka-test + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt new file mode 100644 index 000000000..73ce9d305 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt @@ -0,0 +1,108 @@ +package co.nilin.opex.port.websocket.kafka.config + +import co.nilin.opex.matching.core.eventh.events.CoreEvent +import co.nilin.opex.port.websocket.kafka.consumer.OrderKafkaListener +import co.nilin.opex.port.websocket.kafka.consumer.EventKafkaListener +import co.nilin.opex.port.websocket.kafka.consumer.TradeKafkaListener +import org.apache.kafka.clients.admin.NewTopic +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +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.context.support.GenericApplicationContext +import org.springframework.kafka.core.* +import org.springframework.kafka.listener.ConcurrentMessageListenerContainer +import org.springframework.kafka.listener.ContainerProperties +import org.springframework.kafka.support.serializer.JsonDeserializer +import org.springframework.kafka.support.serializer.JsonSerializer +import java.util.regex.Pattern + +@Configuration +class WebSocketKafkaConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private val bootstrapServers: String? = null + + @Value("\${spring.kafka.consumer.group-id}") + private val groupId: String? = null + + @Bean("websocketConsumerConfig") + fun consumerConfigs(): Map? { + val props: MutableMap = HashMap() + props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers + props[ConsumerConfig.GROUP_ID_CONFIG] = groupId + props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java + props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = JsonDeserializer::class.java + props[JsonDeserializer.TRUSTED_PACKAGES] = "co.nilin.opex.*" + return props + } + + @Bean("websocketConsumerFactory") + fun consumerFactory(@Qualifier("websocketConsumerConfig") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Bean("websocketProducerConfig") + fun producerConfigs(): Map { + val props: MutableMap = HashMap() + props[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers + props[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java + props[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = JsonSerializer::class.java + return props + } + + @Bean("websocketProducerFactory") + fun producerFactory(@Qualifier("websocketProducerConfig") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("websocketKafkaTemplate") + fun kafkaTemplate(@Qualifier("websocketProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } + + + @Autowired + @ConditionalOnBean(TradeKafkaListener::class) + fun configureTradeListener(tradeListener: TradeKafkaListener, @Qualifier("websocketConsumerFactory") consumerFactory: ConsumerFactory) { + val containerProps = ContainerProperties(Pattern.compile("richTrade")) + containerProps.messageListener = tradeListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.beanName = "WebsocketTradeKafkaListenerContainer" + container.start() + } + + @Autowired + @ConditionalOnBean(EventKafkaListener::class) + fun configureEventListener(eventListener: EventKafkaListener, @Qualifier("websocketConsumerFactory") consumerFactory: ConsumerFactory) { + val containerProps = ContainerProperties(Pattern.compile("events_.*")) + containerProps.messageListener = eventListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.beanName = "WebsocketEventKafkaListenerContainer" + container.start() + } + + @Autowired + @ConditionalOnBean(OrderKafkaListener::class) + fun configureOrderListener(orderListener: OrderKafkaListener, @Qualifier("websocketConsumerFactory") consumerFactory: ConsumerFactory) { + val containerProps = ContainerProperties(Pattern.compile("richOrder")) + containerProps.messageListener = orderListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.beanName = "WebsocketOrderKafkaListenerContainer" + container.start() + } + + @Autowired + fun createTopics(applicationContext: GenericApplicationContext) { + applicationContext.registerBean("topic_richOrder", NewTopic::class.java, "richOrder", 10, 1) + applicationContext.registerBean("topic_richTrade", NewTopic::class.java, "richTrade", 10, 1) + } + + +} \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt new file mode 100644 index 000000000..9a398d7b4 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.port.websocket.kafka.consumer + + +import co.nilin.opex.matching.core.eventh.events.CoreEvent +import co.nilin.opex.port.websocket.kafka.spi.EventListener +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/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt new file mode 100644 index 000000000..1bf5082f5 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.port.websocket.kafka.consumer + +import co.nilin.opex.accountant.core.inout.RichOrder +import co.nilin.opex.port.websocket.kafka.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/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/TradeKafkaListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/TradeKafkaListener.kt new file mode 100644 index 000000000..7072951cd --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/TradeKafkaListener.kt @@ -0,0 +1,29 @@ +package co.nilin.opex.port.websocket.kafka.consumer + +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.port.websocket.kafka.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/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/EventListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/EventListener.kt new file mode 100644 index 000000000..b8e111a40 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/EventListener.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.port.websocket.kafka.spi + +import co.nilin.opex.matching.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/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichOrderListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichOrderListener.kt new file mode 100644 index 000000000..326ef97a6 --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichOrderListener.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.port.websocket.kafka.spi + +import co.nilin.opex.accountant.core.inout.RichOrder + +interface RichOrderListener { + fun id(): String + fun onOrder(order: RichOrder, partition: Int, offset: Long, timestamp: Long) +} \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichTradeListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichTradeListener.kt new file mode 100644 index 000000000..b6c596c4f --- /dev/null +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/spi/RichTradeListener.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.port.websocket.kafka.spi + +import co.nilin.opex.accountant.core.inout.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/Websocket/websocket-ports/websocket-persister-postgres/.gitignore b/Websocket/websocket-ports/websocket-persister-postgres/.gitignore new file mode 100644 index 000000000..de5a9214d --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +.mvn/ +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +.DS_Store diff --git a/Api/api-ports/api-websocket/pom.xml b/Websocket/websocket-ports/websocket-persister-postgres/pom.xml similarity index 74% rename from Api/api-ports/api-websocket/pom.xml rename to Websocket/websocket-ports/websocket-persister-postgres/pom.xml index 37e2fd68f..1d1091700 100644 --- a/Api/api-ports/api-websocket/pom.xml +++ b/Websocket/websocket-ports/websocket-persister-postgres/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -9,17 +9,16 @@ co.nilin.opex - api-websocket + websocket-persister-postgres 1.0-SNAPSHOT - api-websocket - Websocket handler + websocket-persister-postgres 1.8 1.4.31 ${version} ${version} - ${version} + ${version} ${version} @@ -32,8 +31,8 @@ co.nilin.opex - api-core - ${api.version} + websocket-core + ${websocket.version} provided @@ -50,28 +49,19 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-data-r2dbc - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - - org.springframework.boot - spring-boot-starter-websocket - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security - spring-security-messaging + io.r2dbc + r2dbc-postgresql + runtime - com.fasterxml.jackson.module - jackson-module-kotlin + org.postgresql + postgresql + runtime + io.projectreactor.kotlin reactor-kotlin-extensions @@ -88,10 +78,14 @@ org.jetbrains.kotlinx kotlinx-coroutines-reactor + - org.springframework.boot - spring-boot-starter-test - test + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + com.google.code.gson + gson io.projectreactor @@ -126,4 +120,11 @@ + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/config/PostgresConfig.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/config/PostgresConfig.kt new file mode 100644 index 000000000..0f54814cc --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/config/PostgresConfig.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.port.websocket.postgres.config + +import org.springframework.context.annotation.Configuration +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories + +@Configuration +@EnableR2dbcRepositories(basePackages = ["co.nilin.opex"]) +class PostgresConfig diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/OrderRepository.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/OrderRepository.kt new file mode 100644 index 000000000..a3338e733 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/OrderRepository.kt @@ -0,0 +1,106 @@ +package co.nilin.opex.port.websocket.postgres.dao + +import co.nilin.opex.matching.core.model.OrderDirection +import co.nilin.opex.websocket.core.inout.AggregatedOrderPriceModel +import co.nilin.opex.port.websocket.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 where uuid = :uuid and (:symbol is null or symbol = :symbol) and status in (:statuses)") + 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(executed_qty)) as quantity from orders + where symbol = :symbol and side = :direction and status in (:statuses) + 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_qty)) as quantity from orders + where symbol = :symbol and side = :direction and status in (:statuses) + 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/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/SymbolMapRepository.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/SymbolMapRepository.kt new file mode 100644 index 000000000..5d266accc --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/SymbolMapRepository.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.port.websocket.postgres.dao + +import co.nilin.opex.port.websocket.postgres.model.SymbolMapModel +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 SymbolMapRepository : ReactiveCrudRepository { + + @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 diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/TradeRepository.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/TradeRepository.kt new file mode 100644 index 000000000..0dec743b2 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/dao/TradeRepository.kt @@ -0,0 +1,150 @@ +package co.nilin.opex.port.websocket.postgres.dao + +import co.nilin.opex.port.websocket.postgres.model.CandleInfoData +import co.nilin.opex.port.websocket.postgres.model.TradeModel +import co.nilin.opex.port.websocket.postgres.model.TradeTickerData +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.time.LocalDateTime +import java.util.* + +@Repository +interface TradeRepository : ReactiveCrudRepository { + + @Query("select * from trades where :ouid in (taker_ouid, maker_ouid) ") + fun findByOuid(@Param("ouid") ouid: String): Flow + + @Query( + """ + select * from trades where :uuid in (taker_uuid, maker_uuid) + and (:fromTrade is null or id > :fromTrade) + and (:symbol is null or symbol = :symbol) + and (:startTime is null or trade_date >= :startTime) + and (:endTime is null or trade_date < :endTime) + """ + ) + fun findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan( + @Param("uuid") + uuid: String, + @Param("symbol") + symbol: String?, + @Param("fromTrade") + fromTrade: Long?, + @Param("startTime") + startTime: Date?, + @Param("endTime") + endTime: Date? + ): Flow + + @Query("select * from trades where symbol = :symbol order by create_date desc limit :limit") + fun findBySymbolSortDescendingByCreateDate( + @Param("symbol") + symbol: String, + @Param("limit") + limit: Int + ): Flow + + @Query( + """ + select symbol, + (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date asc limit 1) as price_change, + ((((select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date asc limit 1))/(select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date asc limit 1))*100) as price_change_percent, + (sum(matched_quantity)/sum(taker_price)) as weighted_avg_price, + (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date asc limit 1) as last_price, + (select matched_quantity from trades where create_date > :date and symbol=t.symbol order by create_date asc limit 1) as last_qty, + (select price from orders where create_date > :date and symbol=t.symbol and (status=1 or status=4) and side='BID' order by create_date desc limit 1) as bid_price, + (select price from orders where create_date > :date and symbol=t.symbol and (status=1 or status=4) and side='ASK' order by create_date asc limit 1) as ask_price, + (select price from orders where create_date > :date and symbol=t.symbol and (status=1 or status=4) order by create_date desc limit 1) as open_price, + max(taker_price) as high_price, + min(taker_price) as low_price, + sum(matched_quantity) as volume, + (select id from trades where create_date > :date and symbol=t.symbol order by create_date asc limit 1) as first_id, + (select id from trades where create_date > :date and symbol=t.symbol order by create_date desc limit 1) as last_id, + count(id) as count + from trades as t + where create_date > :date + group by symbol + """ + ) + fun tradeTicker(@Param("date") createDate: LocalDateTime): Flux + + @Query( + """ + select symbol, + (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date asc limit 1) as price_change, + ((((select taker_price from trades where create_date > :date and symbol=:symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date asc limit 1))/(select taker_price from trades where create_date > :date and symbol=:symbol order by create_date asc limit 1))*100) as price_change_percent, + (sum(matched_quantity)/sum(taker_price)) as weighted_avg_price, + (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date asc limit 1) as last_price, + (select matched_quantity from trades where create_date > :date and symbol=:symbol order by create_date asc limit 1) as last_qty, + (select price from orders where create_date > :date and symbol=t.symbol and (status=1 or status=4) and side='BID' order by create_date desc limit 1) as bid_price, + (select price from orders where create_date > :date and symbol=t.symbol and (status=1 or status=4) and side='ASK' order by create_date asc limit 1) as ask_price, + (select price from orders where create_date > :date and symbol=t.symbol and (status=1 or status=4) order by create_date desc limit 1) as open_price, + max(taker_price) as high_price, + min(taker_price) as low_price, + sum(matched_quantity) as volume, + (select id from trades where create_date > :date and symbol=:symbol order by create_date asc limit 1) as first_id, + (select id from trades where create_date > :date and symbol=:symbol order by create_date desc limit 1) as last_id, + count(id) as count + from trades as t + where create_date > :date and symbol = :symbol + group by symbol + """ + ) + fun tradeTickerBySymbol( + @Param("symbol") + symbol: String, + @Param("date") + createDate: LocalDateTime, + ): Mono + + @Query("select * from trades where create_date in (select max(create_date) from trades group by symbol) and symbol = :symbol") + fun findBySymbolGroupBySymbol(@Param("symbol") symbol: String): Flux + + @Query("select * from trades where create_date in (select max(create_date) from trades group by symbol)") + fun findAllGroupBySymbol(): Flux + + @Query( + """ + with intervals as (select * from interval_generator((:startTime), (:endTime), :interval ::INTERVAL)) + select + f.start_time as open_time, + f.end_time as close_time, + (select taker_price from trades tt where symbol = :symbol and tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date asc limit 1) as open, + max(t.taker_price) as high, + min(t.taker_price) as low, + (select taker_price from trades tt where symbol = :symbol and tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date desc limit 1) as close, + sum(t.matched_quantity) as volume, + count(id) as trades + from trades t + right join intervals f + on t.create_date >= f.start_time and t.create_date < f.end_time + where symbol = :symbol or symbol is null + group by f.start_time, f.end_time + order by f.end_time desc + limit :limit + """ + ) + suspend fun candleData( + @Param("symbol") + symbol: String, + @Param("interval") + interval: String, + @Param("startTime") + startTime: LocalDateTime, + @Param("endTime") + endTime: LocalDateTime, + @Param("limit") + limit: Int, + ): Flux + + @Query("select * from trades order by create_date desc limit 1") + suspend fun findLastByCreateDate(): Mono + + @Query("select * from trades order by create_date asc limit 1") + suspend fun findFirstByCreateDate(): Mono +} \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/MarketQueryHandlerImpl.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/MarketQueryHandlerImpl.kt new file mode 100644 index 000000000..eb676717a --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/MarketQueryHandlerImpl.kt @@ -0,0 +1,209 @@ +package co.nilin.opex.port.websocket.postgres.impl + +import co.nilin.opex.websocket.core.inout.* +import co.nilin.opex.websocket.core.spi.MarketQueryHandler +import co.nilin.opex.websocket.core.spi.SymbolMapper +import co.nilin.opex.matching.core.model.OrderDirection +import co.nilin.opex.port.websocket.postgres.dao.OrderRepository +import co.nilin.opex.port.websocket.postgres.dao.TradeRepository +import co.nilin.opex.port.websocket.postgres.model.OrderModel +import co.nilin.opex.port.websocket.postgres.model.TradeTickerData +import co.nilin.opex.port.websocket.postgres.util.* +import co.nilin.opex.port.websocket.postgres.util.toWebSocketOrderType +import co.nilin.opex.port.websocket.postgres.util.toOrderSide +import co.nilin.opex.port.websocket.postgres.util.toOrderStatus +import co.nilin.opex.port.websocket.postgres.util.toTimeInForce +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.lang.Exception +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 symbolMapper: SymbolMapper, +) : MarketQueryHandler { + + 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? { + return orderRepository.findLastOrderBySymbol(symbol) + .awaitFirstOrNull() + ?.asQueryOrderResponse() + } + + 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 websocketSymbol = try { + symbolMapper.map(it.symbol) + } catch (e: Exception) { + it.symbol + } + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + PriceTickerResponse( + websocketSymbol, + 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() = QueryOrderResponse( + symbol, + ouid, + orderId ?: -1, + -1, + clientOrderId ?: "", + price!!.toBigDecimal(), + quantity!!.toBigDecimal(), + executedQuantity!!.toBigDecimal(), + (accumulativeQuoteQty ?: 0.0).toBigDecimal(), + status!!.toOrderStatus(), + constraint!!.toTimeInForce(), + type!!.toWebSocketOrderType(), + direction!!.toOrderSide(), + null, + null, + Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), + Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), + status.toOrderStatus().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/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/SymbolMapperImpl.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/SymbolMapperImpl.kt new file mode 100644 index 000000000..e29132356 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/SymbolMapperImpl.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.port.websocket.postgres.impl + +import co.nilin.opex.websocket.core.spi.SymbolMapper +import co.nilin.opex.port.websocket.postgres.dao.SymbolMapRepository +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component + +@Component +class SymbolMapperImpl(val symbolMapRepository: SymbolMapRepository) : SymbolMapper { + + override suspend fun map(symbol: String?): String? { + if (symbol == null) return null + return symbolMapRepository.findBySymbol(symbol).awaitFirstOrNull()?.value + } + + override suspend fun unmap(value: String?): String? { + if (value == null) return null + return symbolMapRepository.findByValue(value).awaitFirstOrNull()?.symbol + } + + override suspend fun getKeyValues(): Map { + val map = HashMap() + symbolMapRepository.findAll() + .collectList() + .awaitFirstOrElse { emptyList() } + .forEach { map[it.symbol] = it.value } + return map + } +} \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/UserQueryHandlerImpl.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/UserQueryHandlerImpl.kt new file mode 100644 index 000000000..2e989bcf0 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/impl/UserQueryHandlerImpl.kt @@ -0,0 +1,139 @@ +package co.nilin.opex.port.websocket.postgres.impl + +import co.nilin.opex.websocket.core.inout.* +import co.nilin.opex.websocket.core.spi.UserQueryHandler +import co.nilin.opex.matching.core.model.OrderDirection +import co.nilin.opex.port.websocket.postgres.dao.OrderRepository +import co.nilin.opex.port.websocket.postgres.dao.TradeRepository +import co.nilin.opex.port.websocket.postgres.model.OrderModel +import co.nilin.opex.port.websocket.postgres.util.* +import co.nilin.opex.port.websocket.postgres.util.toWebSocketOrderType +import co.nilin.opex.port.websocket.postgres.util.toOrderSide +import co.nilin.opex.port.websocket.postgres.util.toOrderStatus +import co.nilin.opex.port.websocket.postgres.util.toTimeInForce +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.security.Principal +import java.time.ZoneId +import java.util.* + +@Component +class UserQueryHandlerImpl( + val orderRepository: OrderRepository, + val tradeRepository: TradeRepository +) : 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() + if (order?.constraint != null) { + if (order.uuid != principal.name) + throw OpexException(OpexError.Forbidden) + return orderToQueryResponse(order) + } + return null + } + + 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 { order -> orderToQueryResponse(order) } + } + + 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 { order -> orderToQueryResponse(order) } + } + + 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 orderToQueryResponse(order: OrderModel) = QueryOrderResponse( + order.symbol, + order.ouid, + order.orderId ?: -1, + -1, + order.clientOrderId ?: "", + order.price!!.toBigDecimal(), + order.quantity!!.toBigDecimal(), + order.executedQuantity!!.toBigDecimal(), + (order.accumulativeQuoteQty ?: 0.0).toBigDecimal(), + order.status!!.toOrderStatus(), + order.constraint!!.toTimeInForce(), + order.type!!.toWebSocketOrderType(), + order.direction!!.toOrderSide(), + null, + null, + Date.from(order.createDate!!.atZone(ZoneId.systemDefault()).toInstant()), + Date.from(order.updateDate.atZone(ZoneId.systemDefault()).toInstant()), + order.status.toOrderStatus().isWorking(), order.quoteQuantity!!.toBigDecimal() + ) +} \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/CandleInfoData.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/CandleInfoData.kt new file mode 100644 index 000000000..4fd205c2b --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/CandleInfoData.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.port.websocket.postgres.model + +import org.springframework.data.relational.core.mapping.Column +import java.time.LocalDateTime + +data class CandleInfoData( + @Column("open_time") + val openTime: LocalDateTime, + @Column("close_time") + val closeTime: LocalDateTime, + val open: Double?, + val close: Double?, + val high: Double?, + val low: Double?, + val volume: Double?, + val trades: Int, +) \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/OrderModel.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/OrderModel.kt new file mode 100644 index 000000000..20a1dad95 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/OrderModel.kt @@ -0,0 +1,42 @@ +package co.nilin.opex.port.websocket.postgres.model + + +import co.nilin.opex.matching.core.model.MatchConstraint +import co.nilin.opex.matching.core.model.OrderDirection +import co.nilin.opex.matching.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("executed_qty") val executedQuantity: Double?, + @Column("accumulative_quote_qty") val accumulativeQuoteQty: Double?, + @Column("status") val status: Int?, + @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/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/SymbolMapModel.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/SymbolMapModel.kt new file mode 100644 index 000000000..6c8d483bc --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/SymbolMapModel.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.port.websocket.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 diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeModel.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeModel.kt new file mode 100644 index 000000000..e05a5c785 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeModel.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.port.websocket.postgres.model + + +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("trades") +class TradeModel( + @Id var id: Long?, + @Column("trade_id") val tradeId: Long, + val symbol: String, + @Column("matched_quantity") val matchedQuantity: Double, + @Column("taker_price") val takerPrice: Double, + @Column("maker_price") val makerPrice: Double, + @Column("taker_commision") val takerCommision: Double?, + @Column("maker_commision") val makerCommision: Double?, + @Column("taker_commision_asset") val takerCommisionAsset: String?, + @Column("maker_commision_asset") val makerCommisionAsset: String?, + @Column("trade_date") val tradeDate: LocalDateTime, + @Column("maker_ouid") val makerOuid: String, + @Column("taker_ouid") val takerOuid: String, + @Column("maker_uuid") val makerUuid: String, + @Column("taker_uuid") val takerUuid: String, + @Column("create_date") val createDate: LocalDateTime +) \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeTickerData.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeTickerData.kt new file mode 100644 index 000000000..96f094a0f --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/model/TradeTickerData.kt @@ -0,0 +1,33 @@ +package co.nilin.opex.port.websocket.postgres.model + +import org.springframework.data.relational.core.mapping.Column + +data class TradeTickerData( + val symbol: String, + @Column("price_change") + val priceChange: Double?, + @Column("price_change_percent") + val priceChangePercent: Double?, + @Column("weighted_avg_price") + val weightedAvgPrice: Double?, + @Column("last_price") + val lastPrice: Double?, + @Column("last_qty") + val lastQty: Double?, + @Column("bid_price") + val bidPrice: Double?, + @Column("ask_price") + val askPrice: Double?, + @Column("open_price") + val openPrice: Double?, + @Column("high_price") + val highPrice: Double?, + @Column("low_price") + val lowPrice: Double?, + val volume: Double?, + @Column("first_id") + val firstId: Long?, + @Column("last_id") + val lastId: Long?, + val count: Long?, +) diff --git a/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/util/EnumExtensions.kt b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/util/EnumExtensions.kt new file mode 100644 index 000000000..c84e303a0 --- /dev/null +++ b/Websocket/websocket-ports/websocket-persister-postgres/src/main/kotlin/co/nilin/opex/port/websocket/postgres/util/EnumExtensions.kt @@ -0,0 +1,45 @@ +package co.nilin.opex.port.websocket.postgres.util + +import co.nilin.opex.websocket.core.inout.OrderSide +import co.nilin.opex.websocket.core.inout.OrderStatus +import co.nilin.opex.websocket.core.inout.TimeInForce +import co.nilin.opex.matching.core.model.MatchConstraint +import co.nilin.opex.matching.core.model.OrderDirection +import co.nilin.opex.matching.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.toWebSocketOrderType(): co.nilin.opex.websocket.core.inout.OrderType { + if (this == OrderType.LIMIT_ORDER) + return co.nilin.opex.websocket.core.inout.OrderType.LIMIT + if (this == OrderType.MARKET_ORDER) + return co.nilin.opex.websocket.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) +} \ No newline at end of file diff --git a/Websocket/websocket-root.iml b/Websocket/websocket-root.iml new file mode 100644 index 000000000..4fd5057cb --- /dev/null +++ b/Websocket/websocket-root.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file From 78a847dceaa0b4ced9f89e97a2aebbc8fe92b989 Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 17 Nov 2021 18:21:10 +0330 Subject: [PATCH 4/8] Started creating a system to handle subscribers --- .../websocket/controller/MarketController.kt | 26 +++++++-- .../opex/port/websocket/dto/DepthResponse.kt | 9 +++ .../port/websocket/dto/OrderBookResponse.kt | 9 +++ .../port/websocket/service/MarketService.kt | 39 +++++++++++++ .../websocket/service/MarketStreamHandler.kt | 31 ++++++++++ .../port/websocket/service/SubscriberPool.kt | 39 ------------- .../service/stream/MarketPathType.kt | 21 +++++++ .../port/websocket/service/stream/PathPool.kt | 22 +++++++ .../websocket/service/stream/StreamHandler.kt | 30 ++++++++++ .../service/stream/SubscriberManager.kt | 58 +++++++++++++++++++ .../websocket/service/stream/Subscription.kt | 10 ++++ .../port/websocket/socket/WebSocketConfig.kt | 30 ---------- .../kafka/config/WebSocketKafkaConfig.kt | 12 ---- .../kafka/consumer/EventKafkaListener.kt | 30 ---------- .../kafka/consumer/OrderKafkaListener.kt | 1 - 15 files changed, 250 insertions(+), 117 deletions(-) create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/DepthResponse.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderBookResponse.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt delete mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt delete mode 100644 Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt index 053e433cd..b554e3914 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt @@ -1,15 +1,31 @@ package co.nilin.opex.port.websocket.controller -import org.springframework.messaging.handler.annotation.Header -import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.handler.annotation.DestinationVariable +import org.springframework.messaging.simp.annotation.SubscribeMapping import org.springframework.stereotype.Controller @Controller class MarketController { - @MessageMapping("/market/depth") - fun orderBook(@Header("symbol") symbol:String):List{ - return emptyList() + @SubscribeMapping("/market/depth/{symbol}") + fun orderBook(@DestinationVariable("symbol") symbol: String): String { + + return "Subscribed" + } + + @SubscribeMapping("/market/kline/{symbol}") + fun candleData(@DestinationVariable("symbol") symbol: String): String { + //TODO subscribe user to channel + return "Subscribed" + } + + @SubscribeMapping("/market/ticker/{symbol}-{duration}") + fun priceChange( + @DestinationVariable("symbol") symbol: String, + @DestinationVariable("duration") duration: String + ): String { + //TODO subscribe user to channel + return "Subscribed" } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/DepthResponse.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/DepthResponse.kt new file mode 100644 index 000000000..f3807c9ff --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/DepthResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.port.websocket.dto + +import java.math.BigDecimal + +data class DepthResponse( + val lastUpdateId: Long, + val bids: List>, // Inner list -> [0]: PRICE, [1]: QTY + val asks: List> // Inner list -> [0]: PRICE, [1]: QTY +) \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderBookResponse.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderBookResponse.kt new file mode 100644 index 000000000..91444b51f --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderBookResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.port.websocket.dto + +import java.math.BigDecimal + +data class OrderBookResponse( + val lastUpdateId: Long, + val bids: List>, // Inner list -> [0]: PRICE, [1]: QTY + val asks: List> // Inner list -> [0]: PRICE, [1]: QTY +) \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt new file mode 100644 index 000000000..606c6e081 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt @@ -0,0 +1,39 @@ +package co.nilin.opex.port.websocket.service + +import co.nilin.opex.port.websocket.dto.DepthResponse +import co.nilin.opex.websocket.core.inout.OrderBookResponse +import co.nilin.opex.websocket.core.spi.MarketQueryHandler +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class MarketService(private val marketQueryHandler: MarketQueryHandler) { + + suspend fun getOrderBookDepth(symbol: String): DepthResponse { + val mappedBidOrders = ArrayList>() + val mappedAskOrders = ArrayList>() + + val bidOrders = marketQueryHandler.openBidOrders(symbol, 500) + val askOrders = marketQueryHandler.openAskOrders(symbol, 500) + + bidOrders.forEach { + val mapped = arrayListOf().apply { + add(it.price ?: BigDecimal.ZERO) + add(it.quantity ?: BigDecimal.ZERO) + } + mappedBidOrders.add(mapped) + } + + askOrders.forEach { + val mapped = arrayListOf().apply { + add(it.price ?: BigDecimal.ZERO) + add(it.quantity ?: BigDecimal.ZERO) + } + mappedAskOrders.add(mapped) + } + + val lastOrder = marketQueryHandler.lastOrder(symbol) + return DepthResponse(lastOrder?.orderId ?: -1, mappedBidOrders, mappedAskOrders) + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt new file mode 100644 index 000000000..3eeefd099 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt @@ -0,0 +1,31 @@ +package co.nilin.opex.port.websocket.service + +import co.nilin.opex.port.websocket.service.stream.MarketPathType +import co.nilin.opex.port.websocket.service.stream.StreamHandler +import org.springframework.beans.factory.config.ConfigurableBeanFactory +import org.springframework.context.annotation.Scope +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Component + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) +class MarketStreamHandler(val template: SimpMessagingTemplate) : StreamHandler() { + + fun addOrderBookSub(symbol: String, sessionId: String) { + //TODO validate path + addSubscription("/market/depth/$symbol", MarketPathType.Depth(symbol), sessionId) + } + + fun addCandleDataSub(symbol: String, sessionId: String) { + addSubscription("/market/kline/$symbol", MarketPathType.Candle(symbol), sessionId) + } + + fun priceChange(symbol: String, duration: String, sessionId: String) { + addSubscription("/market/ticker/$symbol-$duration", MarketPathType.Ticker(symbol, duration), sessionId) + } + + override fun isPathSubscribable(path: String): Boolean { + return false + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt deleted file mode 100644 index e5e1cd01f..000000000 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/SubscriberPool.kt +++ /dev/null @@ -1,39 +0,0 @@ -package co.nilin.opex.port.websocket.service - -import org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_SINGLETON -import org.springframework.context.annotation.Scope -import org.springframework.messaging.simp.SimpMessagingTemplate -import org.springframework.stereotype.Component - -@Component -@Scope(SCOPE_SINGLETON) -class SubscriberPool(private val template: SimpMessagingTemplate) { - - private val public = hashSetOf() - private val secured = hashSetOf() - - fun sendPublicMessage(path: String, message: Any) { - if (hasPublicSubscriber()) - template.convertAndSend(path, message) - } - - fun sendSecuredMessage(path: String, user: String, message: Any) { - if (hasSecuredSubscriber()) - template.convertAndSendToUser(user, path, message) - } - - fun addPublicSubscriber(sub: String) { - public.add(sub) - } - - fun addSecuredSubscriber(sub: String) { - secured.add(sub) - } - - fun hasPublicSubscriber() = public.isNotEmpty() - - fun hasSecuredSubscriber() = secured.isNotEmpty() - - fun hasAnySubscriber() = hasPublicSubscriber() && hasSecuredSubscriber() - -} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt new file mode 100644 index 000000000..0b98dfdf0 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.port.websocket.service.stream + +sealed class MarketPathType(val base: String) { + + class Depth(val symbol: String) : MarketPathType("/market/depth") + class Candle(val symbol: String) : MarketPathType("/market/kline") + class Ticker(val symbol: String, val duration: String) : MarketPathType("/market/ticker") + + companion object { + fun isPartOfBases(path: String): Boolean { + return false + } + } +} + +fun String.startsWithAny(vararg list: String): Boolean { + for (l in list) + if (this.startsWith(l)) + return true + return false +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt new file mode 100644 index 000000000..313628dbc --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt @@ -0,0 +1,22 @@ +package co.nilin.opex.port.websocket.service.stream + +import java.security.Principal + +class PathPool(val path: String, val pathType: T) { + + private val subscriptions = arrayListOf() + + fun addSub(sessionId: String, user: Principal? = null) { + subscriptions.add(Subscription(sessionId, user)) + } + + fun removeSub(sessionId: String) { + val sub = subscriptions.find { it.sessionId == sessionId } + if (sub != null) + subscriptions.remove(sub) + } + + fun hasAnySubscriber() = subscriptions.isNotEmpty() + + fun numberOfSubscribers() = subscriptions.size +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt new file mode 100644 index 000000000..438292d41 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.port.websocket.service.stream + +abstract class StreamHandler { + + protected val map = hashMapOf>() + + fun addSubscription(path: String, pathType: T, sessionId: String) { + if (!isPathSubscribable(path)) + return + + if (map[path] == null) { + map[path] = PathPool(path, pathType).apply { addSub(sessionId) } + } else { + map[path]?.addSub(sessionId) + } + } + + fun removeSubscription(path: String, sessionId: String) { + map[path]?.removeSub(sessionId) + } + + fun countSubscribers(): Int { + var sum = 0 + map.entries.forEach { sum += it.value.numberOfSubscribers() } + return sum + } + + abstract fun isPathSubscribable(path: String): Boolean + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt new file mode 100644 index 000000000..2f3f6d2f0 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt @@ -0,0 +1,58 @@ +package co.nilin.opex.port.websocket.service.stream + +import org.springframework.beans.factory.config.ConfigurableBeanFactory +import org.springframework.context.ApplicationListener +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Scope +import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent +import org.springframework.web.socket.messaging.* + +@Configuration +@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) +class SubscriberManager { + + private val handlers = hashSetOf>() + + fun register(handler: StreamHandler<*>) { + handlers.add(handler) + } + + private fun removeSubscription(path: String, sessionId: String) { + handlers.forEach { + it.removeSubscription(path, sessionId) + } + } + + @Bean + fun brokerAvailabilityListener() = ApplicationListener { event -> + println("Is broker available: ${event.isBrokerAvailable}") + } + + @Bean + fun sessionConnectListener() = ApplicationListener { event -> + println("* session connect received: ${event.message}") + } + + @Bean + fun sessionConnectedListener() = ApplicationListener { event -> + println("* connected: ${event.message}") + } + + @Bean + fun sessionDisconnectedListener() = ApplicationListener { event -> + println("* disconnected: ${event.message}") + } + + @Bean + fun sessionSubscribeListener() = ApplicationListener { event -> + val headers = event.message.headers + removeSubscription(headers["simpDestination"] as String, headers["simpSessionId"] as String) + } + + @Bean + fun sessionUnsubscribeEventListener() = ApplicationListener { event -> + println("- unsubscribed: ${event.message}") + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt new file mode 100644 index 000000000..57e2c5435 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.port.websocket.service.stream + +import java.security.Principal +import java.util.* + +data class Subscription( + val sessionId: String, + val user: Principal? = null, + val time: Long = Date().time +) \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt index 4d0854141..501856f08 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt @@ -31,34 +31,4 @@ class WebSocketConfig : WebSocketMessageBrokerConfigurer { } } - @Bean - fun brokerAvailabilityListener() = ApplicationListener { event -> - println("Is broker available: ${event.isBrokerAvailable}") - } - - @Bean - fun sessionConnectListener() = ApplicationListener { event -> - println("* session connect received: ${event.message}") - } - - @Bean - fun sessionConnectedListener() = ApplicationListener { event -> - println("* connected: ${event.message}") - } - - @Bean - fun sessionDisconnectedListener() = ApplicationListener { event -> - println("* disconnected: ${event.message}") - } - - @Bean - fun sessionSubscribeListener() = ApplicationListener { event -> - println("+ subscribed: ${event.message}") - } - - @Bean - fun sessionUnsubscribeEventListener() = ApplicationListener { event -> - println("- unsubscribed: ${event.message}") - } - } \ No newline at end of file diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt index 73ce9d305..905b33c00 100644 --- a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/config/WebSocketKafkaConfig.kt @@ -2,7 +2,6 @@ package co.nilin.opex.port.websocket.kafka.config import co.nilin.opex.matching.core.eventh.events.CoreEvent import co.nilin.opex.port.websocket.kafka.consumer.OrderKafkaListener -import co.nilin.opex.port.websocket.kafka.consumer.EventKafkaListener import co.nilin.opex.port.websocket.kafka.consumer.TradeKafkaListener import org.apache.kafka.clients.admin.NewTopic import org.apache.kafka.clients.consumer.ConsumerConfig @@ -67,7 +66,6 @@ class WebSocketKafkaConfig { return KafkaTemplate(producerFactory) } - @Autowired @ConditionalOnBean(TradeKafkaListener::class) fun configureTradeListener(tradeListener: TradeKafkaListener, @Qualifier("websocketConsumerFactory") consumerFactory: ConsumerFactory) { @@ -78,16 +76,6 @@ class WebSocketKafkaConfig { container.start() } - @Autowired - @ConditionalOnBean(EventKafkaListener::class) - fun configureEventListener(eventListener: EventKafkaListener, @Qualifier("websocketConsumerFactory") consumerFactory: ConsumerFactory) { - val containerProps = ContainerProperties(Pattern.compile("events_.*")) - containerProps.messageListener = eventListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.beanName = "WebsocketEventKafkaListenerContainer" - container.start() - } - @Autowired @ConditionalOnBean(OrderKafkaListener::class) fun configureOrderListener(orderListener: OrderKafkaListener, @Qualifier("websocketConsumerFactory") consumerFactory: ConsumerFactory) { diff --git a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt deleted file mode 100644 index 9a398d7b4..000000000 --- a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/EventKafkaListener.kt +++ /dev/null @@ -1,30 +0,0 @@ -package co.nilin.opex.port.websocket.kafka.consumer - - -import co.nilin.opex.matching.core.eventh.events.CoreEvent -import co.nilin.opex.port.websocket.kafka.spi.EventListener -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/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt index 1bf5082f5..e9879e2ff 100644 --- a/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt +++ b/Websocket/websocket-ports/websocket-eventlistener-kafka/src/main/kotlin/co/nilin/opex/port/websocket/kafka/consumer/OrderKafkaListener.kt @@ -15,7 +15,6 @@ class OrderKafkaListener : MessageListener { orderListeners.forEach { tl -> tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) } - } fun addOrderListener(tl: RichOrderListener) { From d82e39a0e63bace03dd51e8ef88dbb8c5bc5acdb Mon Sep 17 00:00:00 2001 From: Peyman Date: Fri, 19 Nov 2021 21:57:30 +0330 Subject: [PATCH 5/8] edited MarketStreamHandler --- .../opex/port/websocket/config/AppConfig.kt | 5 +- .../port/websocket/config/AppDispatchers.kt | 2 +- .../listener/WebSocketKafkaListener.kt | 7 +-- .../service/EventStreamHandlerImpl.kt | 36 +++++++++++++ .../websocket/service/MarketStreamHandler.kt | 42 +++++++++++++--- .../service/stream/MarketPathType.kt | 18 +++---- .../port/websocket/service/stream/PathPool.kt | 8 +-- .../websocket/service/stream/StreamHandler.kt | 12 +++-- .../websocket/core/dto/EventSubscription.kt | 23 +++++++++ .../websocket/core/spi/EventStreamHandler.kt | 50 +++++++++++++++++++ 10 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt create mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt index 0ecdc37c2..3effe507a 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppConfig.kt @@ -3,6 +3,7 @@ package co.nilin.opex.port.websocket.config import co.nilin.opex.port.websocket.kafka.consumer.OrderKafkaListener import co.nilin.opex.port.websocket.kafka.consumer.TradeKafkaListener import co.nilin.opex.port.websocket.listener.WebSocketKafkaListener +import co.nilin.opex.websocket.core.spi.EventStreamHandler import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -21,8 +22,8 @@ class AppConfig { } @Bean - fun websocketListener(): WebSocketKafkaListener { - return WebSocketKafkaListener() + fun websocketListener(eventStreamHandler: EventStreamHandler): WebSocketKafkaListener { + return WebSocketKafkaListener(eventStreamHandler) } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt index 071845ce1..a6509e0b8 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/AppDispatchers.kt @@ -5,7 +5,7 @@ import java.util.concurrent.Executors object AppDispatchers { - val websocketExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + val websocketExecutor = Executors.newFixedThreadPool(32).asCoroutineDispatcher() val kafkaExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt index c6494794d..0e2f174a4 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt @@ -5,9 +5,10 @@ import co.nilin.opex.accountant.core.inout.RichTrade import co.nilin.opex.port.websocket.config.AppDispatchers import co.nilin.opex.port.websocket.kafka.spi.RichOrderListener import co.nilin.opex.port.websocket.kafka.spi.RichTradeListener +import co.nilin.opex.websocket.core.spi.EventStreamHandler import kotlinx.coroutines.runBlocking -class WebSocketKafkaListener : RichTradeListener, RichOrderListener { +class WebSocketKafkaListener(private val handler: EventStreamHandler) : RichTradeListener, RichOrderListener { override fun id(): String { return "WebSocketKafkaListener" @@ -20,7 +21,7 @@ class WebSocketKafkaListener : RichTradeListener, RichOrderListener { timestamp: Long ) { runBlocking(AppDispatchers.kafkaExecutor) { - //TODO send to user + handler.handleTrade(trade) } } @@ -31,7 +32,7 @@ class WebSocketKafkaListener : RichTradeListener, RichOrderListener { timestamp: Long ) { runBlocking(AppDispatchers.kafkaExecutor) { - //TODO send to user + handler.handleOrder(order) } } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt new file mode 100644 index 000000000..73a980748 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt @@ -0,0 +1,36 @@ +package co.nilin.opex.port.websocket.service + +import co.nilin.opex.accountant.core.inout.RichOrder +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.port.websocket.config.AppDispatchers +import co.nilin.opex.websocket.core.dto.EventType +import co.nilin.opex.websocket.core.spi.EventStreamHandler +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Component + +@Component +class EventStreamHandlerImpl(private val template: SimpMessagingTemplate) : EventStreamHandler() { + + override suspend fun handleOrder(order: RichOrder) { + + } + + override suspend fun handleTrade(trade: RichTrade) { + + } + + suspend fun send(path: String, data: Any, eventType: EventType) { + withContext(AppDispatchers.websocketExecutor) { + template.convertAndSend(path, data) + } + } + + suspend fun sendToUser(path: String, data: Any, uuid: String, eventType: EventType) { + withContext(AppDispatchers.websocketExecutor) { + template.convertAndSendToUser(uuid, path, data) + } + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt index 3eeefd099..264e57f7d 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt @@ -1,31 +1,61 @@ package co.nilin.opex.port.websocket.service +import co.nilin.opex.port.websocket.config.AppDispatchers import co.nilin.opex.port.websocket.service.stream.MarketPathType import co.nilin.opex.port.websocket.service.stream.StreamHandler +import kotlinx.coroutines.runBlocking import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.annotation.Scope import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component @Component @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) -class MarketStreamHandler(val template: SimpMessagingTemplate) : StreamHandler() { +class MarketStreamHandler( + private val service: MarketService, + private val template: SimpMessagingTemplate +) : StreamHandler("/market") { + + override fun isPathSubscribable(path: String): Boolean { + return MarketPathType.isValidPath(path) + } fun addOrderBookSub(symbol: String, sessionId: String) { //TODO validate path - addSubscription("/market/depth/$symbol", MarketPathType.Depth(symbol), sessionId) + addSubscription("/depth/$symbol", MarketPathType.Depth, sessionId, arrayOf(symbol)) } fun addCandleDataSub(symbol: String, sessionId: String) { - addSubscription("/market/kline/$symbol", MarketPathType.Candle(symbol), sessionId) + addSubscription("/kline/$symbol", MarketPathType.Candle, sessionId, arrayOf(symbol)) } fun priceChange(symbol: String, duration: String, sessionId: String) { - addSubscription("/market/ticker/$symbol-$duration", MarketPathType.Ticker(symbol, duration), sessionId) + addSubscription("/ticker/$symbol-$duration", MarketPathType.Ticker, sessionId, arrayOf(symbol, duration)) } - override fun isPathSubscribable(path: String): Boolean { - return false + @Scheduled(fixedDelay = 2000) + private fun interval() { + for (it in map.entries) { + if (!it.value.hasAnySubscriber()) + continue + + runBlocking(AppDispatchers.websocketExecutor) { + when (it.value.pathType) { + MarketPathType.Depth -> orderBook(it.key, it.value.data[0] as String?) + MarketPathType.Candle -> TODO() + MarketPathType.Ticker -> TODO() + } + } + } + } + + private suspend fun orderBook(path: String, symbol: String?) { + if (symbol == null) + return + + val depth = service.getOrderBookDepth(symbol) + template.convertAndSend(path, depth) } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt index 0b98dfdf0..b716f9478 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt @@ -1,21 +1,15 @@ package co.nilin.opex.port.websocket.service.stream -sealed class MarketPathType(val base: String) { +enum class MarketPathType(val base: String) { - class Depth(val symbol: String) : MarketPathType("/market/depth") - class Candle(val symbol: String) : MarketPathType("/market/kline") - class Ticker(val symbol: String, val duration: String) : MarketPathType("/market/ticker") + Depth("/market/depth"), + Candle("/market/kline"), + Ticker("/market/ticker"); companion object { - fun isPartOfBases(path: String): Boolean { - return false + fun isValidPath(path: String): Boolean { + return values().find { path.startsWith(it.base) } != null } } -} -fun String.startsWithAny(vararg list: String): Boolean { - for (l in list) - if (this.startsWith(l)) - return true - return false } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt index 313628dbc..b3ee41f9f 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt @@ -2,15 +2,15 @@ package co.nilin.opex.port.websocket.service.stream import java.security.Principal -class PathPool(val path: String, val pathType: T) { +class PathPool(val path: String, val pathType: T, val data: Array) { - private val subscriptions = arrayListOf() + private val subscriptions = hashSetOf() - fun addSub(sessionId: String, user: Principal? = null) { + fun addSubscription(sessionId: String, user: Principal? = null) { subscriptions.add(Subscription(sessionId, user)) } - fun removeSub(sessionId: String) { + fun removeSubscription(sessionId: String) { val sub = subscriptions.find { it.sessionId == sessionId } if (sub != null) subscriptions.remove(sub) diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt index 438292d41..9222613d1 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt @@ -1,22 +1,22 @@ package co.nilin.opex.port.websocket.service.stream -abstract class StreamHandler { +abstract class StreamHandler(protected val base: String) { protected val map = hashMapOf>() - fun addSubscription(path: String, pathType: T, sessionId: String) { + fun addSubscription(path: String, pathType: T, sessionId: String, data: Array) { if (!isPathSubscribable(path)) return if (map[path] == null) { - map[path] = PathPool(path, pathType).apply { addSub(sessionId) } + map[path] = PathPool(path, pathType, data).apply { addSubscription(sessionId) } } else { - map[path]?.addSub(sessionId) + map[path]?.addSubscription(sessionId) } } fun removeSubscription(path: String, sessionId: String) { - map[path]?.removeSub(sessionId) + map[path]?.removeSubscription(sessionId) } fun countSubscribers(): Int { @@ -25,6 +25,8 @@ abstract class StreamHandler { return sum } + fun getPaths() = map.entries.map { it.key } + abstract fun isPathSubscribable(path: String): Boolean } \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt new file mode 100644 index 000000000..927893b4d --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt @@ -0,0 +1,23 @@ +package co.nilin.opex.websocket.core.dto + +import java.security.Principal +import java.util.* + +data class EventSubscription( + val sessionId: String, + val eventType: EventType, + val symbol: String, + val user: Principal? = null, + val time: Long = Date().time +) + +enum class EventType(val path: String) { + Order("/order"), + Trade("/trade"); + + companion object { + fun findByPath(path: String): EventType? { + return values().find { it.path == path } + } + } +} \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt new file mode 100644 index 000000000..d8bc6b147 --- /dev/null +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt @@ -0,0 +1,50 @@ +package co.nilin.opex.websocket.core.spi + +import co.nilin.opex.accountant.core.inout.RichOrder +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.websocket.core.dto.EventSubscription +import co.nilin.opex.websocket.core.dto.EventType +import java.security.Principal + +abstract class EventStreamHandler { + + protected val subscriptions = arrayListOf() + + fun addSubscription(path: String, sessionId: String, symbol: String, user: Principal?) { + when (EventType.findByPath(path)) { + EventType.Order -> subscriptions.add(EventSubscription(sessionId, EventType.Order, symbol, user)) + EventType.Trade -> subscriptions.add(EventSubscription(sessionId, EventType.Trade, symbol, user)) + null -> { + } + } + } + + fun removeSubscription(sessionId: String) { + val sub = subscriptions.find { it.sessionId == sessionId } + if (sub != null) + subscriptions.remove(sub) + } + + fun hasSubscription(): Boolean { + return subscriptions.isNotEmpty() + } + + fun hasSubscriptionForEvent(event: EventType): Boolean { + for (s in subscriptions) + if (s.eventType == event) + return true + return false + } + + fun hasSubscriptionForEventAndSymbol(event: EventType, symbol: String): Boolean { + for (s in subscriptions) + if (s.eventType == event && s.symbol == symbol) + return true + return false + } + + abstract suspend fun handleOrder(order: RichOrder) + + abstract suspend fun handleTrade(trade: RichTrade) + +} \ No newline at end of file From a15d1bf2f5a0088e86fd184e88f404897cc54309 Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 22 Nov 2021 17:23:59 +0330 Subject: [PATCH 6/8] Finish market streams --- .../websocket/controller/MarketController.kt | 40 ++++--- .../controller/WebSocketController.kt | 30 ------ .../nilin/opex/port/websocket/dto/Interval.kt | 43 ++++++++ .../service/MarketDestinationType.kt | 16 +++ .../port/websocket/service/MarketService.kt | 40 ++++++- .../websocket/service/MarketStreamHandler.kt | 71 +++++-------- .../service/stream/IntervalStreamHandler.kt | 100 ++++++++++++++++++ .../service/stream/MarketPathType.kt | 15 --- .../port/websocket/service/stream/PathPool.kt | 22 ---- .../websocket/service/stream/StreamHandler.kt | 32 ------ .../websocket/service/stream/StreamJob.kt | 9 ++ .../websocket/service/stream/Subscription.kt | 10 -- .../port/websocket/socket/AuthInterceptor.kt | 13 ++- .../StompEventsConfig.kt} | 22 +--- .../port/websocket/socket/WebSocketConfig.kt | 2 - 15 files changed, 267 insertions(+), 198 deletions(-) delete mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/Interval.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt delete mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt delete mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt delete mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamJob.kt delete mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt rename Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/{service/stream/SubscriberManager.kt => socket/StompEventsConfig.kt} (64%) diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt index b554e3914..4891491a7 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt @@ -1,31 +1,37 @@ package co.nilin.opex.port.websocket.controller -import org.springframework.messaging.handler.annotation.DestinationVariable -import org.springframework.messaging.simp.annotation.SubscribeMapping +import co.nilin.opex.port.websocket.service.MarketDestinationType +import co.nilin.opex.port.websocket.service.MarketStreamHandler +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.handler.annotation.Payload import org.springframework.stereotype.Controller @Controller -class MarketController { +class MarketController(private val handler: MarketStreamHandler) { - @SubscribeMapping("/market/depth/{symbol}") - fun orderBook(@DestinationVariable("symbol") symbol: String): String { + data class OrderBookRequest(val symbol: String) + data class PriceTickerRequest(val symbol: String) + data class OverviewTickerRequest(val symbol: String, val duration: String) + data class CandleTickerRequest(val symbol: String, val interval: String) - return "Subscribed" + @MessageMapping("/market/depth") + fun requestOrderBook(@Payload request: OrderBookRequest) { + handler.newSubscription(MarketDestinationType.Depth(request.symbol)) } - @SubscribeMapping("/market/kline/{symbol}") - fun candleData(@DestinationVariable("symbol") symbol: String): String { - //TODO subscribe user to channel - return "Subscribed" + @MessageMapping("/market/price") + fun requestPrice(@Payload request: PriceTickerRequest) { + handler.newSubscription(MarketDestinationType.Price(request.symbol)) } - @SubscribeMapping("/market/ticker/{symbol}-{duration}") - fun priceChange( - @DestinationVariable("symbol") symbol: String, - @DestinationVariable("duration") duration: String - ): String { - //TODO subscribe user to channel - return "Subscribed" + @MessageMapping("/market/overview") + fun requestOverview(@Payload request: OverviewTickerRequest) { + handler.newSubscription(MarketDestinationType.Overview(request.symbol, request.duration)) + } + + @MessageMapping("/market/kline") + fun requestCandleData(@Payload request: CandleTickerRequest) { + handler.newSubscription(MarketDestinationType.Candle(request.symbol, request.interval)) } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt deleted file mode 100644 index 49329e4dd..000000000 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/WebSocketController.kt +++ /dev/null @@ -1,30 +0,0 @@ -package co.nilin.opex.port.websocket.controller - -import org.springframework.messaging.handler.annotation.MessageMapping -import org.springframework.messaging.handler.annotation.SendTo -import org.springframework.messaging.simp.SimpMessagingTemplate -import org.springframework.messaging.simp.annotation.SubscribeMapping -import org.springframework.stereotype.Controller -import java.security.Principal - -@Controller -class WebSocketController(private val template: SimpMessagingTemplate) { - - @SubscribeMapping("/test") - fun test(): String { - print("sss") - return "sss" - } - - @MessageMapping("/a") - @SendTo("/secured/queue/a") - fun a(): String { - return "this is a" - } - - @MessageMapping("/b") - fun b(principal: Principal) { - template.convertAndSendToUser(principal.name,"/secured/queue/b","this is b") - } - -} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/Interval.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/Interval.kt new file mode 100644 index 000000000..d18011976 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/Interval.kt @@ -0,0 +1,43 @@ +package co.nilin.opex.port.websocket.dto + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* +import java.util.concurrent.TimeUnit + +enum class Interval(val label: String, val unit: TimeUnit, val duration: Long) { + + Minute("1m", TimeUnit.MINUTES, 1), + ThreeMinutes("3m", TimeUnit.MINUTES, 3), + FiveMinutes("5m", TimeUnit.MINUTES, 5), + FifteenMinutes("15m", TimeUnit.MINUTES, 15), + ThirtyMinutes("30m", TimeUnit.MINUTES, 30), + Hour("1h", TimeUnit.HOURS, 1), + TwoHours("2h", TimeUnit.HOURS, 2), + FourHours("4h", TimeUnit.HOURS, 4), + SixHours("6h", TimeUnit.HOURS, 6), + EightHours("8h", TimeUnit.HOURS, 8), + TwelveHours("12h", TimeUnit.HOURS, 12), + TwentyFourHours("24h", TimeUnit.HOURS, 24), + Day("1d", TimeUnit.DAYS, 1), + ThreeDays("3d", TimeUnit.DAYS, 3), + Week("1w", TimeUnit.DAYS, 7), + Month("1M", TimeUnit.DAYS, 31), + ThreeMonth("3M",TimeUnit.DAYS, 90); + + private fun getOffsetTime() = unit.toMillis(duration) + + fun getDate() = Date(Date().time - getOffsetTime()) + + fun getLocalDateTime(): LocalDateTime = with(Instant.ofEpochMilli(getDate().time)) { + LocalDateTime.ofInstant(this, ZoneId.systemDefault()) + } + + companion object { + fun findByLabel(label: String): Interval? { + return values().find { it.label == label } + } + } + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt new file mode 100644 index 000000000..4bcf94b41 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt @@ -0,0 +1,16 @@ +package co.nilin.opex.port.websocket.service + +sealed class MarketDestinationType(val base: String, val path: String) { + + data class Depth(val symbol: String) : + MarketDestinationType("/market/depth", "/topic/market/depth/$symbol") + + data class Price(val symbol: String) : + MarketDestinationType("/market/price", "/topic/market/price/$symbol") + + data class Overview(val symbol: String, val duration: String) : + MarketDestinationType("/market/overview", "/topic/market/overview/$symbol-$duration") + + data class Candle(val symbol: String, val interval: String) : + MarketDestinationType("/market/kline", "/topic/market/kline/$symbol-$interval") +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt index 606c6e081..67a858852 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt @@ -1,10 +1,13 @@ package co.nilin.opex.port.websocket.service import co.nilin.opex.port.websocket.dto.DepthResponse -import co.nilin.opex.websocket.core.inout.OrderBookResponse +import co.nilin.opex.port.websocket.dto.Interval +import co.nilin.opex.websocket.core.inout.PriceChangeResponse +import co.nilin.opex.websocket.core.inout.PriceTickerResponse import co.nilin.opex.websocket.core.spi.MarketQueryHandler import org.springframework.stereotype.Service import java.math.BigDecimal +import java.time.ZoneId @Service class MarketService(private val marketQueryHandler: MarketQueryHandler) { @@ -36,4 +39,39 @@ class MarketService(private val marketQueryHandler: MarketQueryHandler) { return DepthResponse(lastOrder?.orderId ?: -1, mappedBidOrders, mappedAskOrders) } + suspend fun getCandleData(symbol: String, duration: String): List> { + val i = Interval.findByLabel(duration) ?: return emptyList() + + val list = ArrayList>() + marketQueryHandler.getCandleInfo(symbol, "${i.duration} ${i.unit}", null, null, 500) + .forEach { + list.add( + arrayListOf( + it.openTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), + it.open.toString(), + it.high.toString(), + it.low.toString(), + it.close.toString(), + it.volume.toString(), + it.closeTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), + it.quoteAssetVolume.toString(), + it.trades, + it.takerBuyBaseAssetVolume.toString(), + it.takerBuyQuoteAssetVolume.toString(), + "0.0" + ) + ) + } + return list + } + + suspend fun getPriceChange(symbol: String): List { + return marketQueryHandler.lastPrice(symbol) + } + + suspend fun getPriceOverview(symbol: String, duration: String): List { + val startDate = Interval.findByLabel(duration)?.getLocalDateTime() ?: Interval.Day.getLocalDateTime() + return listOf(marketQueryHandler.getTradeTickerDataBySymbol(symbol, startDate)) + } + } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt index 264e57f7d..a53635699 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt @@ -1,61 +1,38 @@ package co.nilin.opex.port.websocket.service -import co.nilin.opex.port.websocket.config.AppDispatchers -import co.nilin.opex.port.websocket.service.stream.MarketPathType -import co.nilin.opex.port.websocket.service.stream.StreamHandler -import kotlinx.coroutines.runBlocking -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Scope +import co.nilin.opex.port.websocket.dto.Interval +import co.nilin.opex.port.websocket.service.stream.IntervalStreamHandler +import co.nilin.opex.port.websocket.service.stream.StreamJob import org.springframework.messaging.simp.SimpMessagingTemplate -import org.springframework.scheduling.annotation.Scheduled +import org.springframework.messaging.simp.user.SimpUserRegistry import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit @Component -@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) class MarketStreamHandler( - private val service: MarketService, - private val template: SimpMessagingTemplate -) : StreamHandler("/market") { + private val marketService: MarketService, + template: SimpMessagingTemplate, + userRegistry: SimpUserRegistry +) : IntervalStreamHandler(template, userRegistry) { - override fun isPathSubscribable(path: String): Boolean { - return MarketPathType.isValidPath(path) - } - - fun addOrderBookSub(symbol: String, sessionId: String) { - //TODO validate path - addSubscription("/depth/$symbol", MarketPathType.Depth, sessionId, arrayOf(symbol)) - } - - fun addCandleDataSub(symbol: String, sessionId: String) { - addSubscription("/kline/$symbol", MarketPathType.Candle, sessionId, arrayOf(symbol)) - } - - fun priceChange(symbol: String, duration: String, sessionId: String) { - addSubscription("/ticker/$symbol-$duration", MarketPathType.Ticker, sessionId, arrayOf(symbol, duration)) - } + override fun getPath(type: MarketDestinationType) = type.path - @Scheduled(fixedDelay = 2000) - private fun interval() { - for (it in map.entries) { - if (!it.value.hasAnySubscriber()) - continue - - runBlocking(AppDispatchers.websocketExecutor) { - when (it.value.pathType) { - MarketPathType.Depth -> orderBook(it.key, it.value.data[0] as String?) - MarketPathType.Candle -> TODO() - MarketPathType.Ticker -> TODO() - } + override fun createJob(type: MarketDestinationType) = when (type) { + is MarketDestinationType.Depth -> StreamJob(2, TimeUnit.SECONDS) { + marketService.getOrderBookDepth(type.symbol) + } + is MarketDestinationType.Price -> StreamJob(2, TimeUnit.SECONDS) { + marketService.getPriceChange(type.symbol) + } + is MarketDestinationType.Candle -> { + val i = Interval.findByLabel(type.interval) + StreamJob(i?.duration ?: 2, i?.unit ?: TimeUnit.SECONDS) { + marketService.getCandleData(type.symbol, type.interval) } } - } - - private suspend fun orderBook(path: String, symbol: String?) { - if (symbol == null) - return - - val depth = service.getOrderBookDepth(symbol) - template.convertAndSend(path, depth) + is MarketDestinationType.Overview -> StreamJob(2, TimeUnit.SECONDS) { + marketService.getPriceOverview(type.symbol, type.duration) + } } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt new file mode 100644 index 000000000..eb9ff1f4f --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt @@ -0,0 +1,100 @@ +package co.nilin.opex.port.websocket.service.stream + +import co.nilin.opex.port.websocket.config.AppDispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.messaging.simp.user.SimpUserRegistry +import org.springframework.scheduling.annotation.Scheduled +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture + +abstract class IntervalStreamHandler( + protected val template: SimpMessagingTemplate, + protected val userRegistry: SimpUserRegistry +) { + + private val streamJobs = hashMapOf() + private val jobs = hashMapOf?>() + private val intervalExecutor = Executors.newSingleThreadScheduledExecutor() + private val governorExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val logger = LoggerFactory.getLogger(IntervalStreamHandler::class.java) + + fun newSubscription(type: T) { + registerJob(type, createJob(type)) + logger.info("New subscription added for $type") + } + + private fun registerJob(type: T, job: StreamJob) { + streamJobs[type] = job + runGovernor { + jobs[type] = intervalExecutor.scheduleAtFixedRate( + { job.run(type) }, + 0, + job.interval, + job.timeUnit + ) + } + } + + private fun StreamJob.run(type: T) { + logger.info("job running for $type") + runBlocking(AppDispatchers.websocketExecutor) { + val data = runnable() + template.convertAndSend(getPath(type), data) + } + } + + @Scheduled(fixedDelay = 60 * 1000) + private fun govern() { + runGovernor { + jobs.entries.forEach { j -> + val job = j.value + val count = userRegistry.findSubscriptions { it.destination == getPath(j.key) }.count() + if (count == 0) { + logger.info("No subscriber for ${j.key}. task stopped") + if (job?.isCancelled == false) + job.cancel(false) + } else { + if (job?.isCancelled == true) { + streamJobs[j.key]?.let { s -> + jobs[j.key] = intervalExecutor.scheduleAtFixedRate( + { s.run(j.key) }, + 0, + s.interval, + s.timeUnit + ) + } + logger.info("Starting job") + } + } + } + } + } + + private fun runGovernor(runnable: () -> Unit) { + runBlocking(governorExecutor) { runnable() } + } + + protected fun hasSubscription(): Boolean { + return userRegistry.users.isNotEmpty() + } + + protected fun hasSubscriptionFor(type: T): Boolean { + return userRegistry.findSubscriptions { it.destination == getPath(type) }.isNotEmpty() + } + + protected fun hasSubscriptionForAny(vararg paths: String): Boolean { + return userRegistry.findSubscriptions { paths.contains(it.destination) }.isNotEmpty() + } + + protected fun hasSession(): Boolean { + return userRegistry.users.map { it.sessions }.flatten().isNotEmpty() + } + + protected abstract fun createJob(type: T): StreamJob + + protected abstract fun getPath(type: T): String + +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt deleted file mode 100644 index b716f9478..000000000 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/MarketPathType.kt +++ /dev/null @@ -1,15 +0,0 @@ -package co.nilin.opex.port.websocket.service.stream - -enum class MarketPathType(val base: String) { - - Depth("/market/depth"), - Candle("/market/kline"), - Ticker("/market/ticker"); - - companion object { - fun isValidPath(path: String): Boolean { - return values().find { path.startsWith(it.base) } != null - } - } - -} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt deleted file mode 100644 index b3ee41f9f..000000000 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/PathPool.kt +++ /dev/null @@ -1,22 +0,0 @@ -package co.nilin.opex.port.websocket.service.stream - -import java.security.Principal - -class PathPool(val path: String, val pathType: T, val data: Array) { - - private val subscriptions = hashSetOf() - - fun addSubscription(sessionId: String, user: Principal? = null) { - subscriptions.add(Subscription(sessionId, user)) - } - - fun removeSubscription(sessionId: String) { - val sub = subscriptions.find { it.sessionId == sessionId } - if (sub != null) - subscriptions.remove(sub) - } - - fun hasAnySubscriber() = subscriptions.isNotEmpty() - - fun numberOfSubscribers() = subscriptions.size -} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt deleted file mode 100644 index 9222613d1..000000000 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamHandler.kt +++ /dev/null @@ -1,32 +0,0 @@ -package co.nilin.opex.port.websocket.service.stream - -abstract class StreamHandler(protected val base: String) { - - protected val map = hashMapOf>() - - fun addSubscription(path: String, pathType: T, sessionId: String, data: Array) { - if (!isPathSubscribable(path)) - return - - if (map[path] == null) { - map[path] = PathPool(path, pathType, data).apply { addSubscription(sessionId) } - } else { - map[path]?.addSubscription(sessionId) - } - } - - fun removeSubscription(path: String, sessionId: String) { - map[path]?.removeSubscription(sessionId) - } - - fun countSubscribers(): Int { - var sum = 0 - map.entries.forEach { sum += it.value.numberOfSubscribers() } - return sum - } - - fun getPaths() = map.entries.map { it.key } - - abstract fun isPathSubscribable(path: String): Boolean - -} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamJob.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamJob.kt new file mode 100644 index 000000000..69d544936 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/StreamJob.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.port.websocket.service.stream + +import java.util.concurrent.TimeUnit + +data class StreamJob( + val interval: Long, + val timeUnit: TimeUnit, + val runnable: suspend () -> Any +) \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt deleted file mode 100644 index 57e2c5435..000000000 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/Subscription.kt +++ /dev/null @@ -1,10 +0,0 @@ -package co.nilin.opex.port.websocket.service.stream - -import java.security.Principal -import java.util.* - -data class Subscription( - val sessionId: String, - val user: Principal? = null, - val time: Long = Date().time -) \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt index abec921dd..0efb26c6c 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/AuthInterceptor.kt @@ -8,6 +8,8 @@ import org.springframework.messaging.simp.stomp.StompCommand import org.springframework.messaging.simp.stomp.StompHeaderAccessor import org.springframework.messaging.support.ChannelInterceptor import org.springframework.messaging.support.MessageHeaderAccessor +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.oauth2.jwt.JwtDecoder import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter import org.springframework.stereotype.Component @@ -24,14 +26,19 @@ class AuthInterceptor : ChannelInterceptor { override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> { val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java) if (accessor?.command == StompCommand.CONNECT) { - val authorization = accessor.getNativeHeader("X-Authorization") + val authorization = accessor.getNativeHeader("Authorization") logger.debug("Authorization: $authorization") - if (authorization.isNullOrEmpty()) + if (authorization.isNullOrEmpty()) { + accessor.user = UsernamePasswordAuthenticationToken( + "anonymous", + "N/A", + arrayListOf(SimpleGrantedAuthority("anonymous")) + ) return message + } val token = authorization[0] - val jwt = jwtDecoder.decode(token) val auth = converter.convert(jwt) accessor.user = auth diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/StompEventsConfig.kt similarity index 64% rename from Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt rename to Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/StompEventsConfig.kt index 2f3f6d2f0..8e6e772a7 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/SubscriberManager.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/StompEventsConfig.kt @@ -1,28 +1,13 @@ -package co.nilin.opex.port.websocket.service.stream +package co.nilin.opex.port.websocket.socket -import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.ApplicationListener import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent import org.springframework.web.socket.messaging.* @Configuration -@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) -class SubscriberManager { - - private val handlers = hashSetOf>() - - fun register(handler: StreamHandler<*>) { - handlers.add(handler) - } - - private fun removeSubscription(path: String, sessionId: String) { - handlers.forEach { - it.removeSubscription(path, sessionId) - } - } +class StompEventsConfig { @Bean fun brokerAvailabilityListener() = ApplicationListener { event -> @@ -46,8 +31,7 @@ class SubscriberManager { @Bean fun sessionSubscribeListener() = ApplicationListener { event -> - val headers = event.message.headers - removeSubscription(headers["simpDestination"] as String, headers["simpSessionId"] as String) + println("* subscribed: ${event.message}") } @Bean diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt index 501856f08..96017d6d9 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt @@ -15,8 +15,6 @@ import org.springframework.web.socket.messaging.* @EnableWebSocketMessageBroker class WebSocketConfig : WebSocketMessageBrokerConfigurer { - private val logger = LoggerFactory.getLogger(WebSocketConfig::class.java) - override fun registerStompEndpoints(registry: StompEndpointRegistry) { registry.addEndpoint("/stream") .setAllowedOriginPatterns("*") From a07d74408e0584d2216b3c387d3968a6bd6a7b1f Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 23 Nov 2021 14:25:38 +0330 Subject: [PATCH 7/8] Finish event streams --- Deployment/nginx.conf | 9 ++ .../websocket/config/WebSecurityConfig.kt | 2 +- .../websocket/controller/MarketController.kt | 43 ++++--- .../opex/port/websocket/dto/OrderResponse.kt | 29 +++++ .../port/websocket/dto/RecentTradeResponse.kt | 15 +++ .../opex/port/websocket/dto/TradeResponse.kt | 21 ++++ .../listener/WebSocketKafkaListener.kt | 8 +- .../service/EventStreamHandlerImpl.kt | 109 ++++++++++++++++-- .../service/MarketDestinationType.kt | 6 +- .../port/websocket/service/MarketService.kt | 24 +++- .../websocket/service/MarketStreamHandler.kt | 5 +- .../service/stream/IntervalStreamHandler.kt | 41 ++++--- .../port/websocket/socket/WebSocketConfig.kt | 9 +- .../port/websocket/utils/EnumExtensions.kt | 45 ++++++++ .../src/main/resources/application-docker.yml | 5 +- .../websocket/core/dto/EventSubscription.kt | 23 ---- .../websocket/core/spi/EventStreamHandler.kt | 44 +------ 17 files changed, 311 insertions(+), 127 deletions(-) create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderResponse.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/RecentTradeResponse.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/TradeResponse.kt create mode 100644 Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/utils/EnumExtensions.kt delete mode 100644 Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt diff --git a/Deployment/nginx.conf b/Deployment/nginx.conf index 22c252869..b88719230 100644 --- a/Deployment/nginx.conf +++ b/Deployment/nginx.conf @@ -27,6 +27,10 @@ http { server storage:8096; } + upstream docker-websocket { + server storage:8097; + } + proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -70,6 +74,11 @@ http { rewrite ^/storage/(.*)$ /$1 break; } + location /stream { + proxy_pass http://docker-websocket; + rewrite ^/stream/(.*)$ /$1 break; + } + location /api { proxy_pass http://docker-api; rewrite ^/api(.*)$ $1 break; diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt index 1df8decd7..d1bfa356d 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/config/WebSecurityConfig.kt @@ -25,7 +25,7 @@ class WebSecurityConfig : WebSecurityConfigurerAdapter() { http.httpBasic().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() - .antMatchers("/stream/**").permitAll() + .antMatchers("/ws/**").permitAll() .anyRequest().denyAll() .and() .oauth2ResourceServer() diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt index 4891491a7..f94000ce4 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/controller/MarketController.kt @@ -2,36 +2,47 @@ package co.nilin.opex.port.websocket.controller import co.nilin.opex.port.websocket.service.MarketDestinationType import co.nilin.opex.port.websocket.service.MarketStreamHandler +import org.springframework.messaging.handler.annotation.DestinationVariable import org.springframework.messaging.handler.annotation.MessageMapping import org.springframework.messaging.handler.annotation.Payload +import org.springframework.messaging.simp.annotation.SubscribeMapping import org.springframework.stereotype.Controller @Controller class MarketController(private val handler: MarketStreamHandler) { - data class OrderBookRequest(val symbol: String) - data class PriceTickerRequest(val symbol: String) - data class OverviewTickerRequest(val symbol: String, val duration: String) - data class CandleTickerRequest(val symbol: String, val interval: String) + private val validDurations = arrayOf("24h", "7d", "1M") - @MessageMapping("/market/depth") - fun requestOrderBook(@Payload request: OrderBookRequest) { - handler.newSubscription(MarketDestinationType.Depth(request.symbol)) + @SubscribeMapping("/market/depth/{symbol}") + fun requestOrderBook(@DestinationVariable("symbol") symbol: String) { + handler.newSubscription(MarketDestinationType.Depth(symbol)) } - @MessageMapping("/market/price") - fun requestPrice(@Payload request: PriceTickerRequest) { - handler.newSubscription(MarketDestinationType.Price(request.symbol)) + @SubscribeMapping("/market/price") + fun requestPrice() { + handler.newSubscription(MarketDestinationType.Price) } - @MessageMapping("/market/overview") - fun requestOverview(@Payload request: OverviewTickerRequest) { - handler.newSubscription(MarketDestinationType.Overview(request.symbol, request.duration)) + @SubscribeMapping("/market/overview/{symbol}-{duration}") + fun requestOverview( + @DestinationVariable("symbol") symbol: String, + @DestinationVariable("duration") duration: String + ) { + if (validDurations.contains(duration)) + handler.newSubscription(MarketDestinationType.Overview(symbol, duration)) } - @MessageMapping("/market/kline") - fun requestCandleData(@Payload request: CandleTickerRequest) { - handler.newSubscription(MarketDestinationType.Candle(request.symbol, request.interval)) + @SubscribeMapping("/market/kline/{symbol}-{interval}") + fun requestCandleData( + @DestinationVariable("symbol") symbol: String, + @DestinationVariable("interval") interval: String + ) { + handler.newSubscription(MarketDestinationType.Candle(symbol, interval)) + } + + @SubscribeMapping("/market/recent-trades/{symbol}") + fun requestRecentTrades(@DestinationVariable("symbol") symbol: String) { + handler.newSubscription(MarketDestinationType.RecentTrades(symbol)) } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderResponse.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderResponse.kt new file mode 100644 index 000000000..a5eda691a --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/OrderResponse.kt @@ -0,0 +1,29 @@ +package co.nilin.opex.port.websocket.dto + +import co.nilin.opex.websocket.core.inout.OrderSide +import co.nilin.opex.websocket.core.inout.OrderStatus +import co.nilin.opex.websocket.core.inout.OrderType +import co.nilin.opex.websocket.core.inout.TimeInForce +import java.math.BigDecimal +import java.util.* + +data class OrderResponse( + val symbol: String, + val orderId: Long, + val orderListId: Long, //Unless part of an OCO, the value will always be -1. + val clientOrderId: String?, + val price: BigDecimal, + val origQty: BigDecimal, + val executedQty: BigDecimal, + val cummulativeQuoteQty: BigDecimal, + val status: OrderStatus, + val timeInForce: TimeInForce, + val type: OrderType, + val side: OrderSide, + val stopPrice: BigDecimal?, + val icebergQty: BigDecimal?, + val time: Date, + val updateTime: Date, + val isWorking: Boolean, + val origQuoteOrderQty: BigDecimal +) diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/RecentTradeResponse.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/RecentTradeResponse.kt new file mode 100644 index 000000000..310512ab5 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/RecentTradeResponse.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.port.websocket.dto + +import java.math.BigDecimal +import java.util.* + +data class RecentTradeResponse( + val symbol: String, + val id: Long, + val price: BigDecimal, + val qty: BigDecimal, + val quoteQty: BigDecimal, + val time: Date, + val isBestMatch: Boolean, + val isMakerBuyer: Boolean +) diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/TradeResponse.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/TradeResponse.kt new file mode 100644 index 000000000..0b840aec4 --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/dto/TradeResponse.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.port.websocket.dto + +import java.math.BigDecimal +import java.util.* + +data class TradeResponse( + val symbol: String, + val id: Long, + val orderId: Long, + val orderListId: Long = -1, + val price: BigDecimal, + val qty: BigDecimal, + val quoteQty: BigDecimal, + val commission: BigDecimal, + val commissionAsset: String, + val time: Date, + val isBuyer: Boolean, + val isMaker: Boolean, + val isBestMatch: Boolean, + val isMakerBuyer: Boolean +) \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt index 0e2f174a4..59b50d7b8 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/listener/WebSocketKafkaListener.kt @@ -20,9 +20,7 @@ class WebSocketKafkaListener(private val handler: EventStreamHandler) : RichTrad offset: Long, timestamp: Long ) { - runBlocking(AppDispatchers.kafkaExecutor) { - handler.handleTrade(trade) - } + handler.handleTrade(trade) } override fun onOrder( @@ -31,8 +29,6 @@ class WebSocketKafkaListener(private val handler: EventStreamHandler) : RichTrad offset: Long, timestamp: Long ) { - runBlocking(AppDispatchers.kafkaExecutor) { - handler.handleOrder(order) - } + handler.handleOrder(order) } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt index 73a980748..918e5eeab 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/EventStreamHandlerImpl.kt @@ -2,34 +2,119 @@ package co.nilin.opex.port.websocket.service import co.nilin.opex.accountant.core.inout.RichOrder import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.matching.core.model.OrderDirection import co.nilin.opex.port.websocket.config.AppDispatchers -import co.nilin.opex.websocket.core.dto.EventType +import co.nilin.opex.port.websocket.dto.OrderResponse +import co.nilin.opex.port.websocket.postgres.dao.OrderRepository +import co.nilin.opex.port.websocket.postgres.model.OrderModel +import co.nilin.opex.port.websocket.utils.* +import co.nilin.opex.websocket.core.inout.OrderStatus +import co.nilin.opex.websocket.core.inout.TradeResponse import co.nilin.opex.websocket.core.spi.EventStreamHandler -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.messaging.simp.user.SimpUserRegistry import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.ZoneId +import java.util.* @Component -class EventStreamHandlerImpl(private val template: SimpMessagingTemplate) : EventStreamHandler() { - - override suspend fun handleOrder(order: RichOrder) { +class EventStreamHandlerImpl( + private val template: SimpMessagingTemplate, + private val orderRepository: OrderRepository, + private val registry: SimpUserRegistry +) : EventStreamHandler { + override fun handleOrder(order: RichOrder) { + val response = OrderResponse( + order.pair, + order.orderId ?: -1, + -1, + null, + order.price, + order.quantity, + order.executedQuantity, + order.accumulativeQuoteQty, + order.status.toOrderStatus(), + order.constraint.toTimeInForce(), + order.type.toWebsocketOrderType(), + order.direction.toOrderSide(), + null, + null, + Date(), + Date(), + order.status.toOrderStatus().isWorking(), + order.quoteQuantity + ) + run { template.convertAndSendToUser(order.uuid, EventDestinations.Order.path, response) } } - override suspend fun handleTrade(trade: RichTrade) { + override fun handleTrade(trade: RichTrade) { + run { + val takerOrder = orderRepository.findByOuid(trade.takerOuid).awaitFirstOrNull() + val makerOrder = orderRepository.findByOuid(trade.makerOuid).awaitFirstOrNull() + if (makerOrder==null ||takerOrder==null) + return@run + val maker = trade.buildTradeResponse(trade.makerUuid, makerOrder, takerOrder) + val taker = trade.buildTradeResponse(trade.takerUuid, makerOrder, takerOrder) + template.convertAndSendToUser(trade.makerUuid, EventDestinations.Trade.path, maker) + template.convertAndSendToUser(trade.takerUuid, EventDestinations.Trade.path, taker) + } } - suspend fun send(path: String, data: Any, eventType: EventType) { - withContext(AppDispatchers.websocketExecutor) { - template.convertAndSend(path, data) + private fun RichTrade.buildTradeResponse( + uuid: String, + makerOrder: OrderModel, + takerOrder: OrderModel + ): TradeResponse { + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + return TradeResponse( + pair, + id, + if (takerUuid == uuid) takerOrder.orderId!! else makerOrder.orderId!!, + -1, + if (takerUuid == uuid) takerPrice else makerPrice, + matchedQuantity, + if (isMakerBuyer) + makerOrder.quoteQuantity?.toBigDecimal() ?: BigDecimal.ZERO + else + takerOrder.quoteQuantity?.toBigDecimal() ?: BigDecimal.ZERO, + if (takerUuid == uuid) takerCommision else makerCommision, + if (takerUuid == uuid) takerCommisionAsset else makerCommisionAsset, + Date(), + if (takerUuid == uuid) + OrderDirection.ASK == takerOrder.direction + else + OrderDirection.ASK == makerOrder.direction, + makerUuid == uuid, + true, + isMakerBuyer + ) + } + + private fun run(action: suspend () -> Unit) { + runBlocking(AppDispatchers.websocketExecutor) { + try { + action() + } catch (e: Exception) { + e.printStackTrace() + } } } - suspend fun sendToUser(path: String, data: Any, uuid: String, eventType: EventType) { - withContext(AppDispatchers.websocketExecutor) { - template.convertAndSendToUser(uuid, path, data) + enum class EventDestinations(val path: String) { + Order("/secured/queue/orders"), + Trade("/secured/queue/trades"); + + companion object { + fun findByPath(path: String): EventDestinations? { + return values().find { it.path == path } + } } } diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt index 4bcf94b41..9e6d5fcb8 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketDestinationType.kt @@ -5,12 +5,14 @@ sealed class MarketDestinationType(val base: String, val path: String) { data class Depth(val symbol: String) : MarketDestinationType("/market/depth", "/topic/market/depth/$symbol") - data class Price(val symbol: String) : - MarketDestinationType("/market/price", "/topic/market/price/$symbol") + object Price : MarketDestinationType("/market/price", "/topic/market/price") data class Overview(val symbol: String, val duration: String) : MarketDestinationType("/market/overview", "/topic/market/overview/$symbol-$duration") data class Candle(val symbol: String, val interval: String) : MarketDestinationType("/market/kline", "/topic/market/kline/$symbol-$interval") + + data class RecentTrades(val symbol: String) : + MarketDestinationType("/market/recent-trades", "/topic/market/recent-trades/$symbol") } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt index 67a858852..8eab79bfd 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketService.kt @@ -2,9 +2,12 @@ package co.nilin.opex.port.websocket.service import co.nilin.opex.port.websocket.dto.DepthResponse import co.nilin.opex.port.websocket.dto.Interval +import co.nilin.opex.port.websocket.dto.RecentTradeResponse import co.nilin.opex.websocket.core.inout.PriceChangeResponse import co.nilin.opex.websocket.core.inout.PriceTickerResponse import co.nilin.opex.websocket.core.spi.MarketQueryHandler +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList import org.springframework.stereotype.Service import java.math.BigDecimal import java.time.ZoneId @@ -65,8 +68,8 @@ class MarketService(private val marketQueryHandler: MarketQueryHandler) { return list } - suspend fun getPriceChange(symbol: String): List { - return marketQueryHandler.lastPrice(symbol) + suspend fun getPriceChange(): List { + return marketQueryHandler.lastPrice(null) } suspend fun getPriceOverview(symbol: String, duration: String): List { @@ -74,4 +77,21 @@ class MarketService(private val marketQueryHandler: MarketQueryHandler) { return listOf(marketQueryHandler.getTradeTickerDataBySymbol(symbol, startDate)) } + suspend fun getRecentTrades(symbol: String): List { + return marketQueryHandler.recentTrades(symbol, 500) + .map { + RecentTradeResponse( + it.symbol, + it.id, + it.price, + it.qty, + it.quoteQty, + it.time, + it.isBestMatch, + it.isMakerBuyer + ) + } + .toList() + } + } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt index a53635699..f1ba2b546 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/MarketStreamHandler.kt @@ -22,7 +22,7 @@ class MarketStreamHandler( marketService.getOrderBookDepth(type.symbol) } is MarketDestinationType.Price -> StreamJob(2, TimeUnit.SECONDS) { - marketService.getPriceChange(type.symbol) + marketService.getPriceChange() } is MarketDestinationType.Candle -> { val i = Interval.findByLabel(type.interval) @@ -33,6 +33,9 @@ class MarketStreamHandler( is MarketDestinationType.Overview -> StreamJob(2, TimeUnit.SECONDS) { marketService.getPriceOverview(type.symbol, type.duration) } + is MarketDestinationType.RecentTrades -> StreamJob(2, TimeUnit.SECONDS) { + marketService.getRecentTrades(type.symbol) + } } } \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt index eb9ff1f4f..6bb51464c 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/service/stream/IntervalStreamHandler.kt @@ -9,37 +9,47 @@ import org.springframework.messaging.simp.user.SimpUserRegistry import org.springframework.scheduling.annotation.Scheduled import java.util.concurrent.Executors import java.util.concurrent.ScheduledFuture +import java.util.concurrent.ScheduledThreadPoolExecutor abstract class IntervalStreamHandler( protected val template: SimpMessagingTemplate, - protected val userRegistry: SimpUserRegistry + private val userRegistry: SimpUserRegistry ) { private val streamJobs = hashMapOf() private val jobs = hashMapOf?>() - private val intervalExecutor = Executors.newSingleThreadScheduledExecutor() + private val intervalExecutor = ScheduledThreadPoolExecutor(1) private val governorExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val logger = LoggerFactory.getLogger(IntervalStreamHandler::class.java) + init { + intervalExecutor.removeOnCancelPolicy = true + } + fun newSubscription(type: T) { - registerJob(type, createJob(type)) + registerJob(type) logger.info("New subscription added for $type") } - private fun registerJob(type: T, job: StreamJob) { - streamJobs[type] = job + private fun registerJob(type: T) { runGovernor { - jobs[type] = intervalExecutor.scheduleAtFixedRate( - { job.run(type) }, - 0, - job.interval, - job.timeUnit - ) + if (streamJobs[type] == null) + streamJobs[type] = createJob(type) + + val job = streamJobs[type] ?: return@runGovernor + if (jobs[type] == null || jobs[type]?.isCancelled == true) { + logger.info("job running for $type") + jobs[type] = intervalExecutor.scheduleAtFixedRate( + { job.run(type) }, + 0, + job.interval, + job.timeUnit + ) + } } } private fun StreamJob.run(type: T) { - logger.info("job running for $type") runBlocking(AppDispatchers.websocketExecutor) { val data = runnable() template.convertAndSend(getPath(type), data) @@ -53,11 +63,12 @@ abstract class IntervalStreamHandler( val job = j.value val count = userRegistry.findSubscriptions { it.destination == getPath(j.key) }.count() if (count == 0) { - logger.info("No subscriber for ${j.key}. task stopped") - if (job?.isCancelled == false) + if (job?.isCancelled == false) { + logger.info("No subscriber for ${j.key}. task stopped") job.cancel(false) + } } else { - if (job?.isCancelled == true) { + if (job == null || job.isCancelled) { streamJobs[j.key]?.let { s -> jobs[j.key] = intervalExecutor.scheduleAtFixedRate( { s.run(j.key) }, diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt index 96017d6d9..db18d95a2 100644 --- a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/socket/WebSocketConfig.kt @@ -1,22 +1,17 @@ package co.nilin.opex.port.websocket.socket -import org.slf4j.LoggerFactory -import org.springframework.context.ApplicationListener -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent import org.springframework.messaging.simp.config.MessageBrokerRegistry import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker import org.springframework.web.socket.config.annotation.StompEndpointRegistry import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer -import org.springframework.web.socket.messaging.* @Configuration @EnableWebSocketMessageBroker class WebSocketConfig : WebSocketMessageBrokerConfigurer { override fun registerStompEndpoints(registry: StompEndpointRegistry) { - registry.addEndpoint("/stream") + registry.addEndpoint("/ws") .setAllowedOriginPatterns("*") .withSockJS() } @@ -24,7 +19,7 @@ class WebSocketConfig : WebSocketMessageBrokerConfigurer { override fun configureMessageBroker(registry: MessageBrokerRegistry) { with(registry) { enableSimpleBroker("/topic", "/secured/queue") - setApplicationDestinationPrefixes("/app") + setApplicationDestinationPrefixes("/app", "/topic") //setUserDestinationPrefix("/secured/user") } } diff --git a/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/utils/EnumExtensions.kt b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/utils/EnumExtensions.kt new file mode 100644 index 000000000..959e714cb --- /dev/null +++ b/Websocket/websocket-app/src/main/kotlin/co/nilin/opex/port/websocket/utils/EnumExtensions.kt @@ -0,0 +1,45 @@ +package co.nilin.opex.port.websocket.utils + +import co.nilin.opex.websocket.core.inout.OrderSide +import co.nilin.opex.websocket.core.inout.OrderStatus +import co.nilin.opex.websocket.core.inout.TimeInForce +import co.nilin.opex.matching.core.model.MatchConstraint +import co.nilin.opex.matching.core.model.OrderDirection +import co.nilin.opex.matching.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.toWebsocketOrderType(): co.nilin.opex.websocket.core.inout.OrderType { + if (this == OrderType.LIMIT_ORDER) + return co.nilin.opex.websocket.core.inout.OrderType.LIMIT + if (this == OrderType.MARKET_ORDER) + return co.nilin.opex.websocket.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) +} \ No newline at end of file diff --git a/Websocket/websocket-app/src/main/resources/application-docker.yml b/Websocket/websocket-app/src/main/resources/application-docker.yml index 2e088ab93..81262acea 100644 --- a/Websocket/websocket-app/src/main/resources/application-docker.yml +++ b/Websocket/websocket-app/src/main/resources/application-docker.yml @@ -12,4 +12,7 @@ spring: host: ${CONSUL_HOST} port: 8500 main: - allow-bean-definition-overriding: true \ No newline at end of file + allow-bean-definition-overriding: true +app: + auth: + cert-url: http://auth:8083/auth/realms/opex/protocol/openid-connect/certs \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt deleted file mode 100644 index 927893b4d..000000000 --- a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/dto/EventSubscription.kt +++ /dev/null @@ -1,23 +0,0 @@ -package co.nilin.opex.websocket.core.dto - -import java.security.Principal -import java.util.* - -data class EventSubscription( - val sessionId: String, - val eventType: EventType, - val symbol: String, - val user: Principal? = null, - val time: Long = Date().time -) - -enum class EventType(val path: String) { - Order("/order"), - Trade("/trade"); - - companion object { - fun findByPath(path: String): EventType? { - return values().find { it.path == path } - } - } -} \ No newline at end of file diff --git a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt index d8bc6b147..d4002ba70 100644 --- a/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt +++ b/Websocket/websocket-core/src/main/kotlin/co/nilin/opex/websocket/core/spi/EventStreamHandler.kt @@ -2,49 +2,11 @@ package co.nilin.opex.websocket.core.spi import co.nilin.opex.accountant.core.inout.RichOrder import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.websocket.core.dto.EventSubscription -import co.nilin.opex.websocket.core.dto.EventType -import java.security.Principal -abstract class EventStreamHandler { +interface EventStreamHandler { - protected val subscriptions = arrayListOf() + fun handleOrder(order: RichOrder) - fun addSubscription(path: String, sessionId: String, symbol: String, user: Principal?) { - when (EventType.findByPath(path)) { - EventType.Order -> subscriptions.add(EventSubscription(sessionId, EventType.Order, symbol, user)) - EventType.Trade -> subscriptions.add(EventSubscription(sessionId, EventType.Trade, symbol, user)) - null -> { - } - } - } - - fun removeSubscription(sessionId: String) { - val sub = subscriptions.find { it.sessionId == sessionId } - if (sub != null) - subscriptions.remove(sub) - } - - fun hasSubscription(): Boolean { - return subscriptions.isNotEmpty() - } - - fun hasSubscriptionForEvent(event: EventType): Boolean { - for (s in subscriptions) - if (s.eventType == event) - return true - return false - } - - fun hasSubscriptionForEventAndSymbol(event: EventType, symbol: String): Boolean { - for (s in subscriptions) - if (s.eventType == event && s.symbol == symbol) - return true - return false - } - - abstract suspend fun handleOrder(order: RichOrder) - - abstract suspend fun handleTrade(trade: RichTrade) + fun handleTrade(trade: RichTrade) } \ No newline at end of file From 86529e92f3c0035dc60952e138de53a5ace810fc Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 23 Nov 2021 14:31:52 +0330 Subject: [PATCH 8/8] Update jenkins script --- Jenkins/Jenkinsfile.deploy.groovy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jenkins/Jenkinsfile.deploy.groovy b/Jenkins/Jenkinsfile.deploy.groovy index 2e336d280..29305ef4c 100644 --- a/Jenkins/Jenkinsfile.deploy.groovy +++ b/Jenkins/Jenkinsfile.deploy.groovy @@ -47,6 +47,10 @@ pipeline { dir("Storage") { sh 'mvn -B clean install' } + + dir("Websocket") { + sh 'mvn -B clean install' + } } }