diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dd84ea782..320592668 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
+
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
+
+- OS: [e.g. iOS]
+- Browser [e.g. chrome, safari]
+- Version [e.g. 22]
**Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
+
+- Device: [e.g. iPhone6]
+- OS: [e.g. iOS8.1]
+- Browser [e.g. stock browser, safari]
+- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
diff --git a/.github/workflows/dev-otc.yml b/.github/workflows/dev-otc.yml
index b05c794ba..9aa39b90e 100644
--- a/.github/workflows/dev-otc.yml
+++ b/.github/workflows/dev-otc.yml
@@ -1,8 +1,8 @@
-name: Build, Test, and Deploy otc (DEV env) services for specific partner
+name: Build, Test, and Deploy otc (DEV env) services for specific partner
on:
-# push:
-# branches:
-# - dev
+ # push:
+ # branches:
+ # - dev
workflow_dispatch:
inputs:
@@ -29,10 +29,9 @@ jobs:
uses: actions/checkout@v2
- name: Setup Java
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
- distribution: 'adopt'
- java-package: jdk
+ distribution: temurin
java-version: ${{ matrix.java }}
- name: Fetch partners config
@@ -69,7 +68,6 @@ jobs:
echo "PASSWORD=$PASSWORD" >> $GITHUB_OUTPUT
echo "ENV_PATH=$ENV_PATH" >> $GITHUB_OUTPUT
echo "SSH_PORT=$SSH_PORT" >> $GITHUB_OUTPUT
-
- name: Decrypt data
id: decrypt_data
@@ -107,9 +105,9 @@ jobs:
echo "password=$server_pass" >> $GITHUB_OUTPUT
- name: Build
- run: |
- mvn -pl common -am -B -T 1C clean install -Potc
- mvn -pl wallet,bc-gateway -amd -B -T 1C clean install -Potc
+ run: |
+ mvn -pl common -am -B clean install -Potc
+ mvn -pl wallet,bc-gateway -amd -B clean install -Potc
- name: Run Tests
run: |
diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index 8cd20f339..a0409266b 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -16,16 +16,15 @@ jobs:
- name: Checkout Source Code
uses: actions/checkout@v2
- name: Setup Java
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
- distribution: 'adopt'
- java-package: jdk
+ distribution: temurin
java-version: ${{ matrix.java }}
-# cache: maven
+ # cache: maven
- name: Build
- run: mvn -B -T 1C clean install
+ run: mvn -B clean install
- name: Run Tests
- run: mvn -B -T 1C -Dskip.unit.tests=false surefire:test
+ run: mvn -B -Dskip.unit.tests=false surefire:test
- name: Build Docker images
env:
TAG: dev
diff --git a/.github/workflows/main-otc.yml b/.github/workflows/main-otc.yml
index 4a0d3ca45..16c18ea9c 100644
--- a/.github/workflows/main-otc.yml
+++ b/.github/workflows/main-otc.yml
@@ -1,8 +1,8 @@
-name: Build, Test, and Deploy otc (PRD env) services for specific partner
+name: Build, Test, and Deploy otc (PRD env) services for specific partner
on:
-# push:
-# branches:
-# - main
+ # push:
+ # branches:
+ # - main
workflow_dispatch:
inputs:
@@ -29,10 +29,9 @@ jobs:
uses: actions/checkout@v2
- name: Setup Java
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
- distribution: 'adopt'
- java-package: jdk
+ distribution: temurin
java-version: ${{ matrix.java }}
- name: Fetch partners config
@@ -107,14 +106,14 @@ jobs:
echo "password=$server_pass" >> $GITHUB_OUTPUT
- name: Build
- run: |
+ run: |
mvn -pl common -am -B -T 1C clean install -Potc
mvn -pl wallet,bc-gateway -amd -B -T 1C clean install -Potc
- name: Run Tests
run: |
- mvn -pl common -am -B -T 1C -Dskip.unit.tests=false surefire:test
- mvn -pl wallet,bc-gateway -amd -B -T 1C -Dskip.unit.tests=false surefire:test
+ mvn -pl common -am -B -Dskip.unit.tests=false surefire:test
+ mvn -pl wallet,bc-gateway -amd -B -Dskip.unit.tests=false surefire:test
- name: Build Docker images
run: docker compose -f docker-compose-otc.build.yml build
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 382536bdb..331e18fb0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,16 +16,15 @@ jobs:
- name: Checkout Source Code
uses: actions/checkout@v2
- name: Setup Java
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
- distribution: 'adopt'
- java-package: jdk
+ distribution: temurin
java-version: ${{ matrix.java }}
-# cache: maven
+ # cache: maven
- name: Build
- run: mvn -B -T 1C clean install
+ run: mvn -B clean install
- name: Run Tests
- run: mvn -B -T 1C -Dskip.unit.tests=false surefire:test
+ run: mvn -B -Dskip.unit.tests=false surefire:test
- name: Build Docker images
env:
TAG: latest
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index ec4f902d6..74af2bad1 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -15,17 +15,16 @@ jobs:
- name: Checkout Source Code
uses: actions/checkout@v2
- name: Setup Java
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
- - name: Verify Docker
- run: docker info
- name: Build
- run: mvn -B -T 1C clean install -Potc
+ run: mvn -B clean install -Potc
- name: Run Tests
- run: mvn -B -T 1C -Dskip.unit.tests=false surefire:test
+ run: mvn -B -Dskip.unit.tests=false surefire:test
+
- name: Build Docker images
env:
TAG: pr
- run: docker compose -f docker-compose.build.yml build
+ run: docker compose -f docker-compose.build.yml build
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7f42c10ed..05f9a75c0 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,7 +5,12 @@ on:
# branches:
# - dev
workflow_dispatch:
-
+# inputs:
+# partner_name:
+# type: string
+# description: 'The name of the partner (provided during workflow execution)'
+# required: true
+# default: default
jobs:
build:
runs-on: ubuntu-22.04
@@ -17,10 +22,9 @@ jobs:
- name: Checkout Source Code
uses: actions/checkout@v2
- name: Setup Java
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
- distribution: 'adopt'
- java-package: jdk
+ distribution: temurin
java-version: ${{ matrix.java }}
# cache: maven
- name: Build
diff --git a/.gitignore b/.gitignore
index cc293eb45..e27020d7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,7 @@ target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
-
+./api.csv
### STS ###
.apt_generated
.classpath
@@ -44,3 +44,4 @@ build/
!/.mvn/
.DS_Store
+realms
\ No newline at end of file
diff --git a/README.md b/README.md
index f15258b05..c10eb526a 100644
--- a/README.md
+++ b/README.md
@@ -57,13 +57,12 @@ PANEL_PASS=admin
BACKEND_USER=admin
KEYCLOAK_ADMIN_USERNAME=opex
KEYCLOAK_ADMIN_PASSWORD=hiopex
-SMTP_PASS=x
+SMTP_PASSWORD=x
API_KEY_CLIENT_SECRET=x
KEYCLOAK_FRONTEND_URL=http://localhost:8083/auth
KEYCLOAK_ADMIN_URL=http://localhost:8083/auth
KEYCLOAK_VERIFY_REDIRECT_URL=http://localhost:8080/verify
KEYCLOAK_FORGOT_REDIRECT_URL=http://localhost:8080/forgot
-PREFERENCES=preferences-demo.yml
WHITELIST_REGISTER_ENABLED=true
WHITELIST_LOGIN_ENABLED=true
WALLET_BACKUP_ENABLED=false
@@ -77,10 +76,9 @@ TAG=debug
| BACKEND_USER | Username used by services to access vault data. Also used as the username for Vault admin panel |
| KEYCLOAK_ADMIN_USERNAME | Username for Keycloak admin panel |
| KEYCLOAK_ADMIN_PASSWORD | Password for Keycloak admin panel |
-| SMTP_PASS | SMTP password used by keycloak to send emails for various operations (e.g. user verification, reset password) |
+| SMTP_PASSWORD | SMTP password used by keycloak to send emails for various operations (e.g. user verification, reset password) |
| API_KEY_CLIENT_SECRET | In order to access the api key feature, please follow the steps below:1. Go to Keycloak admin panel located at http://localhost:8083/auth/admin/master/console/#/realms/opex/clients
2. Login with the username and password you provided in the `.env` file (KEYCLOAK_ADMIN_USERNAME and KEYCLOAK_ADMIN_PASSWORD)
3. Go to `clients` section on the left menu 4. Click on `opex-api-key` client 5. In the credentials section, click on `Regenerate Secret` button 6. Copy the generated secret and paste it into this section |
| KEYCLOAK_FRONTEND_URL
KEYCLOAK_ADMIN_URL
KEYCLOAK_VERIFY_REDIRECT_URL
KEYCLOAK_FORGOT_REDIRECT_URL | Replace `localhost` with your server's IP if you're not running on local machine. Do not change the rest. |
-| PREFERENCES | Points to a file containing seed data used to by modules to initialize their databases. An example of this file is provided and is available inside the root directory (preferences-demo.yml). It's deprecated and will be removed soon |
| WHITELIST_REGISTER_ENABLED | Allows registration only for whitelisted emails |
| WHITELIST_LOGIN_ENABLED | Allows login only for whitelisted emails |
| WALLET_BACKUP_ENABLED | Enables wallet data backup to google drive folder. In order to use this feature, you need to have `drive-key.json` file (obtained from google drive API panel) in the root directory of project |
diff --git a/accountant/accountant-app/Dockerfile b/accountant/accountant-app/Dockerfile
index 9c566c1b2..29f0c2703 100644
--- a/accountant/accountant-app/Dockerfile
+++ b/accountant/accountant-app/Dockerfile
@@ -1,4 +1,4 @@
-FROM openjdk:11
+FROM eclipse-temurin:17-jdk
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
diff --git a/accountant/accountant-app/pom.xml b/accountant/accountant-app/pom.xml
index f722891eb..f56aee06a 100644
--- a/accountant/accountant-app/pom.xml
+++ b/accountant/accountant-app/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -51,6 +51,10 @@
co.nilin.opex.utility
error-handler
+
+ co.nilin.opex.utility
+ interceptors
+
org.springframework.cloud
spring-cloud-starter-vault-config
@@ -64,10 +68,6 @@
micrometer-registry-prometheus
runtime
-
- co.nilin.opex.utility
- preferences
-
org.testcontainers
testcontainers
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt
index 02bc1d88d..ad5272a2c 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/AccountantApp.kt
@@ -1,7 +1,6 @@
package co.nilin.opex.accountant.app
import org.springframework.boot.autoconfigure.SpringBootApplication
-import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt
index f63ed9800..4a160cefb 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt
@@ -1,10 +1,6 @@
package co.nilin.opex.accountant.app.config
import co.nilin.opex.accountant.app.listener.*
-import co.nilin.opex.accountant.app.listener.AccountantEventListener
-import co.nilin.opex.accountant.app.listener.AccountantTempEventListener
-import co.nilin.opex.accountant.app.listener.AccountantTradeListener
-import co.nilin.opex.accountant.app.listener.OrderListener
import co.nilin.opex.accountant.core.api.FeeCalculator
import co.nilin.opex.accountant.core.api.FinancialActionJobManager
import co.nilin.opex.accountant.core.api.OrderManager
@@ -14,19 +10,21 @@ import co.nilin.opex.accountant.core.service.OrderManagerImpl
import co.nilin.opex.accountant.core.service.TradeManagerImpl
import co.nilin.opex.accountant.core.spi.*
import co.nilin.opex.accountant.ports.kafka.listener.consumer.*
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener
-import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener
import co.nilin.opex.accountant.ports.kafka.listener.spi.FAResponseListener
import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling
@Configuration
@EnableScheduling
-class AppConfig {
+class AppConfig(
+ @Value("\${app.trade-volume-calculation-currency}")
+ private val tradeVolumeCalculationCurrency: String,
+ @Value("\${app.zone-offset}")
+ private val zoneOffsetString: String,
+) {
@Bean
fun getFinancialActionJobManager(
@@ -44,7 +42,6 @@ class AppConfig {
@Bean
fun orderManager(
pairConfigLoader: PairConfigLoader,
- userLevelLoader: UserLevelLoader,
financialActionPersister: FinancialActionPersister,
financeActionLoader: FinancialActionLoader,
orderPersister: OrderPersister,
@@ -52,18 +49,17 @@ class AppConfig {
tempEventRepublisher: TempEventRepublisher,
richOrderPublisher: RichOrderPublisher,
financialActionPublisher: FinancialActionPublisher,
- jsonMapper: JsonMapper
+ feeCalculator: FeeCalculator
): OrderManager {
return OrderManagerImpl(
pairConfigLoader,
- userLevelLoader,
financialActionPersister,
financeActionLoader,
orderPersister,
tempEventPersister,
richOrderPublisher,
financialActionPublisher,
- jsonMapper
+ feeCalculator
)
}
@@ -77,7 +73,8 @@ class AppConfig {
richOrderPublisher: RichOrderPublisher,
feeCalculator: FeeCalculator,
financialActionPublisher: FinancialActionPublisher,
- jsonMapper: JsonMapper
+ currencyRatePersister: CurrencyRatePersister,
+ userVolumePersister: UserTradeVolumePersister
): TradeManager {
return TradeManagerImpl(
financeActionPersister,
@@ -88,7 +85,10 @@ class AppConfig {
richOrderPublisher,
feeCalculator,
financialActionPublisher,
- jsonMapper
+ currencyRatePersister,
+ userVolumePersister,
+ tradeVolumeCalculationCurrency,
+ zoneOffsetString
)
}
@@ -137,18 +137,32 @@ class AppConfig {
fun configureEventListener(
eventKafkaListener: EventKafkaListener,
accountantEventListener: AccountantEventListener,
- kycLevelUpdatedKafkaListener: KycLevelUpdatedKafkaListener,
- kycLevelUpdatedEventListener: KycLevelUpdatedListener
) {
eventKafkaListener.addListener(accountantEventListener)
- kycLevelUpdatedKafkaListener.addEventListener(kycLevelUpdatedEventListener)
}
@Autowired
fun configureTempEventListener(
tempEventKafkaListener: TempEventKafkaListener,
- accountantTempEventListener: AccountantTempEventListener) {
+ accountantTempEventListener: AccountantTempEventListener
+ ) {
tempEventKafkaListener.addListener(accountantTempEventListener)
}
+ @Autowired
+ fun configureWithdrawRequestEventListener(
+ withdrawRequestKafkaListener: WithdrawRequestKafkaListener,
+ withdrawRequestEventListener: WithdrawRequestEventListener
+ ) {
+ withdrawRequestKafkaListener.addListener(withdrawRequestEventListener)
+ }
+
+
+ @Autowired
+ fun configureDepositEventListener(
+ depositKafkaListener: DepositKafkaListener,
+ depositEventListener: DepositEventListener
+ ) {
+ depositKafkaListener.addListener(depositEventListener)
+ }
}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt
index 20d751529..8e9b73c94 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt
@@ -1,61 +1,18 @@
package co.nilin.opex.accountant.app.config
-import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository
-import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository
-import co.nilin.opex.accountant.ports.postgres.dao.UserLevelRepository
-import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel
-import co.nilin.opex.utility.preferences.Preferences
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.runBlocking
-import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.DependsOn
import org.springframework.stereotype.Component
import javax.annotation.PostConstruct
@Component
@DependsOn("postgresConfig")
-class InitializeService(
- private val pairConfigRepository: PairConfigRepository,
- private val pairFeeConfigRepository: PairFeeConfigRepository,
- private val userLevelRepository: UserLevelRepository,
-) {
-
- @Autowired
- private lateinit var preferences: Preferences
+class InitializeService{
@PostConstruct
fun init() = runBlocking {
- preferences.userLevels.forEach {
- userLevelRepository.insert(it).awaitSingleOrNull()
- }
-
- preferences.markets.map {
- val pair = it.pair ?: "${it.leftSide}_${it.rightSide}"
- val leftSideCurrency = preferences.currencies.first { c -> it.leftSide == c.symbol }
- val rightSideCurrency = preferences.currencies.first { c -> it.rightSide == c.symbol }
- val leftSideFraction = (it.leftSideFraction ?: leftSideCurrency.precision)
- val rightSideFraction = (it.rightSideFraction ?: rightSideCurrency.precision)
- pairConfigRepository.insert(
- pair,
- it.leftSide,
- it.rightSide,
- leftSideFraction,
- rightSideFraction
- ).awaitSingleOrNull()
- it.feeConfigs.forEach { f ->
- runCatching {
- pairFeeConfigRepository.save(
- PairFeeConfigModel(
- null,
- pair,
- f.direction,
- f.userLevel,
- f.makerFee,
- f.takerFee
- )
- ).awaitSingleOrNull()
- }
- }
- }
+ // addUserLevels()
+ // addPairConfigs()
+ // addPairFeeConfigs
}
}
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/SecurityConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/SecurityConfig.kt
new file mode 100644
index 000000000..dd1dc06dd
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/SecurityConfig.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.accountant.app.config
+
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+
+@EnableWebFluxSecurity
+class SecurityConfig {
+
+ @Bean
+ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ http.csrf().disable()
+ .authorizeExchange()
+ .anyExchange().permitAll()
+ return http.build()
+ }
+}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt
index 3971bbae2..9b0aa7200 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt
@@ -3,6 +3,7 @@ package co.nilin.opex.accountant.app.controller
import co.nilin.opex.accountant.core.model.WalletType
import co.nilin.opex.accountant.core.spi.FinancialActionLoader
import co.nilin.opex.accountant.core.spi.WalletProxy
+import co.nilin.opex.accountant.core.spi.WithdrawLimitManager
import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse
import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent
import org.slf4j.LoggerFactory
@@ -14,8 +15,10 @@ import java.math.BigDecimal
@RestController
class AccountantController(
val walletProxy: WalletProxy,
- val financialActionLoader: FinancialActionLoader
-) {
+ val financialActionLoader: FinancialActionLoader,
+ val withdrawLimitManager: WithdrawLimitManager,
+
+ ) {
private val logger = LoggerFactory.getLogger(AccountantController::class.java)
@@ -36,5 +39,15 @@ class AccountantController(
BooleanResponse(false)
}
+ @GetMapping("{uuid}/{level}/request_withdraw/{amount}_{currency}/allowed")
+ suspend fun canRequestWithdraw(
+ @PathVariable("uuid") uuid: String,
+ @PathVariable("level") userLevel: String,
+ @PathVariable("currency") currency: String,
+ @PathVariable("amount") amount: BigDecimal
+ ): Boolean {
+ return withdrawLimitManager.canWithdraw(uuid, userLevel, currency, amount)
+ }
+
}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/ConfigController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/ConfigController.kt
new file mode 100644
index 000000000..ed04a79fc
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/ConfigController.kt
@@ -0,0 +1,45 @@
+package co.nilin.opex.accountant.app.controller
+
+import co.nilin.opex.accountant.core.model.FeeConfig
+import co.nilin.opex.accountant.core.model.PairConfig
+import co.nilin.opex.accountant.core.model.WithdrawLimitConfig
+import co.nilin.opex.accountant.core.spi.FeeConfigService
+import co.nilin.opex.accountant.core.spi.PairConfigLoader
+import co.nilin.opex.accountant.core.spi.WithdrawLimitManager
+import co.nilin.opex.matching.engine.core.model.OrderDirection
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/config")
+class ConfigController(
+ private val pairConfigLoader: PairConfigLoader,
+ private val feeConfigService: FeeConfigService,
+ private val withdrawLimitManager: WithdrawLimitManager
+) {
+
+ @GetMapping("/{pair}/{direction}")
+ suspend fun fetchPairConfig(
+ @PathVariable("pair") pair: String,
+ @PathVariable("direction") direction: OrderDirection
+ ): PairConfig {
+ return pairConfigLoader.load(pair, direction)
+ }
+
+ @GetMapping("/all")
+ suspend fun fetchPairConfigs(): List {
+ return pairConfigLoader.loadPairConfigs()
+ }
+
+ @GetMapping("/fee")
+ suspend fun getFeeConfigs(): List {
+ return feeConfigService.loadFeeConfigs()
+ }
+
+ @GetMapping("/withdraw-limit")
+ suspend fun getWithdrawLimitConfigs(): List {
+ return withdrawLimitManager.getAll()
+ }
+}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt
deleted file mode 100644
index 962218b17..000000000
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package co.nilin.opex.accountant.app.controller
-
-import co.nilin.opex.accountant.app.data.PairFeeResponse
-import co.nilin.opex.accountant.core.model.PairConfig
-import co.nilin.opex.accountant.core.model.PairFeeConfig
-import co.nilin.opex.accountant.core.spi.PairConfigLoader
-import co.nilin.opex.common.OpexError
-import co.nilin.opex.matching.engine.core.model.OrderDirection
-import org.springframework.web.bind.annotation.*
-
-@RestController
-@RequestMapping("/config")
-class PairConfigController(private val pairConfigLoader: PairConfigLoader) {
-
- @GetMapping("/{pair}/fee/{direction}-{userLevel}")
- suspend fun fetchPairFeeConfig(
- @PathVariable("pair") pair: String,
- @PathVariable("direction") direction: OrderDirection,
- @PathVariable("userLevel") level: String
- ): PairFeeConfig {
- return pairConfigLoader.load(pair, direction, level)
- }
-
- @GetMapping("/{pair}/{direction}")
- suspend fun fetchPairConfig(
- @PathVariable("pair") pair: String,
- @PathVariable("direction") direction: OrderDirection
- ): PairConfig {
- return pairConfigLoader.load(pair, direction)
- }
-
- @GetMapping("/all")
- suspend fun fetchPairConfigs(): List {
- return pairConfigLoader.loadPairConfigs()
- }
-
- @GetMapping("/fee")
- suspend fun getFeeConfigs(): List {
- return pairConfigLoader.loadPairFeeConfigs()
- .map { PairFeeResponse(it.pairConfig.pair, it.direction, it.userLevel, it.makerFee, it.takerFee) }
- }
-
- @GetMapping("/fee/{pair}")
- suspend fun getFeeConfig(
- @PathVariable pair: String,
- @RequestParam(required = false) direction: OrderDirection?,
- @RequestParam(required = false) userLevel: String?
- ): PairFeeResponse {
- val fee = pairConfigLoader.loadPairFeeConfigs(pair, direction ?: OrderDirection.BID, userLevel ?: "*")
- ?: throw OpexError.PairFeeNotFound.exception()
- return PairFeeResponse(fee.pairConfig.pair, fee.direction, fee.userLevel, fee.makerFee, fee.takerFee)
- }
-}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/UserDailyActivityController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/UserDailyActivityController.kt
new file mode 100644
index 000000000..e447f7646
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/UserDailyActivityController.kt
@@ -0,0 +1,46 @@
+package co.nilin.opex.accountant.app.controller
+
+import co.nilin.opex.accountant.core.api.DepositActivityManager
+import co.nilin.opex.accountant.core.api.TradeActivityManager
+import co.nilin.opex.accountant.core.api.WithdrawActivityManager
+import co.nilin.opex.accountant.core.model.DailyAmount
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/user-activity")
+class UserDailyActivityController(
+ private val withdrawManager: WithdrawActivityManager,
+ private val depositManager: DepositActivityManager,
+ private val TradeManager: TradeActivityManager
+) {
+
+ @GetMapping("/withdraw/{userId}")
+ suspend fun getDailyWithdrawLast31Days(
+ @PathVariable userId: String
+ ): List {
+ return withdrawManager.getLastDaysWithdrawActivity(
+ userId = userId
+ )
+ }
+
+ @GetMapping("/deposit/{userId}")
+ suspend fun getDailyDepositLast31Days(
+ @PathVariable userId: String
+ ): List {
+ return depositManager.getLastDaysDepositActivity(
+ userId = userId
+ )
+ }
+
+ @GetMapping("/trade/{userId}")
+ suspend fun getDailyTradeLast31Days(
+ @PathVariable userId: String
+ ): List {
+ return TradeManager.getLastDaysTradeActivity(
+ userId = userId
+ )
+ }
+}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/UserDataController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/UserDataController.kt
new file mode 100644
index 000000000..4066e8730
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/UserDataController.kt
@@ -0,0 +1,64 @@
+package co.nilin.opex.accountant.app.controller
+
+import co.nilin.opex.accountant.core.api.FeeCalculator
+import co.nilin.opex.accountant.core.model.UserFee
+import co.nilin.opex.accountant.core.spi.UserTradeVolumePersister
+import co.nilin.opex.accountant.core.spi.UserWithdrawVolumePersister
+import co.nilin.opex.common.utils.Interval
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.web.bind.annotation.*
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+@RestController
+@RequestMapping("/user/data")
+class UserDataController(
+ private val userVolumePersister: UserTradeVolumePersister,
+ private val feeCalculator: FeeCalculator,
+ private val userWithdrawVolumePersister: UserWithdrawVolumePersister,
+ @Value("\${app.trade-volume-calculation-currency}")
+ private val tradeVolumeCalculationCurrency: String,
+) {
+ @GetMapping("/fee/{uuid}")
+ suspend fun getUserFee(@PathVariable uuid: String): UserFee {
+ return feeCalculator.getUserFee(uuid)
+ }
+
+ @GetMapping("/trade/volume/{uuid}")
+ suspend fun getTradeVolumeByCurrency(
+ @PathVariable uuid: String,
+ @RequestParam symbol: String,
+ @RequestParam interval: Interval
+ ): BigDecimal {
+ return userVolumePersister.getUserTotalTradeVolumeByCurrency(
+ uuid,
+ symbol,
+ interval.getLocalDateTime().toLocalDate(),
+ tradeVolumeCalculationCurrency
+ ) ?: BigDecimal.ZERO
+ }
+
+ @GetMapping("/trade/volume/total/{uuid}")
+ suspend fun getTotalTradeVolumeValue(
+ @PathVariable uuid: String,
+ @RequestParam interval: Interval
+ ): BigDecimal {
+ return userVolumePersister.getUserTotalTradeVolume(
+ uuid,
+ interval.getLocalDateTime().toLocalDate(),
+ tradeVolumeCalculationCurrency
+ ) ?: BigDecimal.ZERO
+ }
+
+ @GetMapping("/withdraw/volume/total/{uuid}")
+ suspend fun getWithdrawVolumeValue(
+ @PathVariable uuid: String,
+ @RequestParam(required = false) interval: Interval?
+ ): BigDecimal {
+ return userWithdrawVolumePersister.getTotalValueByUserAndDateAfter(
+ uuid,
+ (interval?.getLocalDateTime() ?: LocalDateTime.now())
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt
index cbfb4c63f..6ac8d804b 100644
--- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/data/PairFeeResponse.kt
@@ -3,7 +3,7 @@ package co.nilin.opex.accountant.app.data
import java.math.BigDecimal
data class PairFeeResponse(
- val pair:String,
+ val pair: String,
val direction: String,
val userLevel: String,
val makerFee: BigDecimal,
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/DepositEventListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/DepositEventListener.kt
new file mode 100644
index 000000000..01a2662d0
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/DepositEventListener.kt
@@ -0,0 +1,38 @@
+package co.nilin.opex.accountant.app.listener
+
+import co.nilin.opex.accountant.core.spi.UserDepositVolumePersister
+import co.nilin.opex.accountant.ports.kafka.listener.inout.DepositEvent
+import co.nilin.opex.accountant.ports.kafka.listener.inout.WithdrawRequestEvent
+import co.nilin.opex.accountant.ports.kafka.listener.spi.DepositListener
+import co.nilin.opex.accountant.ports.kafka.listener.spi.WithdrawRequestListener
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+
+@Component
+class DepositEventListener(private val userDepositVolumePersister: UserDepositVolumePersister) :
+ DepositListener {
+
+ private val logger = LoggerFactory.getLogger(DepositEventListener::class.java)
+ val scope = CoroutineScope(Dispatchers.IO)
+ override fun id(): String {
+ return "DepositEventListener"
+ }
+
+ override fun onEvent(
+ event: DepositEvent,
+ partition: Int,
+ offset: Long,
+ timestamp: Long
+ ) {
+ logger.info("==========================================================================")
+ logger.info("Incoming Deposit event: $event")
+ logger.info("==========================================================================")
+ scope.launch {
+ userDepositVolumePersister.update(event.uuid, event.currency, event.amount, event.createDate)
+ }
+ }
+
+}
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/WithdrawRequestEventListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/WithdrawRequestEventListener.kt
new file mode 100644
index 000000000..d2a3ace87
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/WithdrawRequestEventListener.kt
@@ -0,0 +1,36 @@
+package co.nilin.opex.accountant.app.listener
+
+import co.nilin.opex.accountant.core.spi.UserWithdrawVolumePersister
+import co.nilin.opex.accountant.ports.kafka.listener.inout.WithdrawRequestEvent
+import co.nilin.opex.accountant.ports.kafka.listener.spi.WithdrawRequestListener
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+
+@Component
+class WithdrawRequestEventListener(private val userWithdrawVolumePersister: UserWithdrawVolumePersister) :
+ WithdrawRequestListener {
+
+ private val logger = LoggerFactory.getLogger(WithdrawRequestEventListener::class.java)
+ val scope = CoroutineScope(Dispatchers.IO)
+ override fun id(): String {
+ return "WithdrawRequestEventListener"
+ }
+
+ override fun onEvent(
+ event: WithdrawRequestEvent,
+ partition: Int,
+ offset: Long,
+ timestamp: Long
+ ) {
+ logger.info("==========================================================================")
+ logger.info("Incoming WithdrawRequest event: $event")
+ logger.info("==========================================================================")
+ scope.launch {
+ userWithdrawVolumePersister.update(event.uuid, event.currency, event.amount, event.createDate, event.status)
+ }
+ }
+
+}
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/CurrencyPriceJob.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/CurrencyPriceJob.kt
new file mode 100644
index 000000000..149b5e71f
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/CurrencyPriceJob.kt
@@ -0,0 +1,42 @@
+package co.nilin.opex.accountant.app.scheduler
+
+import co.nilin.opex.accountant.core.spi.CurrencyRatePersister
+import co.nilin.opex.accountant.core.spi.WalletProxy
+import kotlinx.coroutines.runBlocking
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.scheduling.annotation.Scheduled
+import org.springframework.stereotype.Service
+import java.math.BigDecimal
+
+
+@Service
+class CurrencyPriceJob(
+ private val currencyRatePersister: CurrencyRatePersister,
+ private val walletProxy: WalletProxy,
+ @Value("\${app.trade-volume-calculation-currency}")
+ private val tradeVolumeCalculationCurrency: String,
+ @Value("\${app.withdraw-volume-calculation-currency}")
+ private val withdrawVolumeCalculationCurrency: String
+) {
+
+ @Scheduled(fixedDelay = 120_000, initialDelay = 120_000)
+ fun updatePrices() {
+ runBlocking {
+ updateCurrencyRates(tradeVolumeCalculationCurrency)
+
+ if (tradeVolumeCalculationCurrency != withdrawVolumeCalculationCurrency) {
+ updateCurrencyRates(withdrawVolumeCalculationCurrency)
+ }
+ }
+ }
+ private suspend fun updateCurrencyRates(quoteCurrency: String) {
+ walletProxy.getPrices(quoteCurrency)
+ .forEach { price ->
+ currencyRatePersister.updateRate(
+ price.currency,
+ quoteCurrency,
+ price.sellPrice ?: BigDecimal.ZERO
+ )
+ }
+ }
+}
diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/utils/Extensions.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/utils/Extensions.kt
new file mode 100644
index 000000000..e9914a2cb
--- /dev/null
+++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/utils/Extensions.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.accountant.app.utils
+
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.*
+
+fun LocalDateTime.asDate(): Date {
+ return Date.from(atZone(ZoneId.systemDefault()).toInstant())
+}
+
+fun Date.asLocalDateTime(): LocalDateTime {
+ return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault())
+}
+
+fun Long.asLocalDateTime(): LocalDateTime {
+ return LocalDateTime.ofInstant(Date(this).toInstant(), ZoneId.systemDefault())
+}
\ No newline at end of file
diff --git a/accountant/accountant-app/src/main/resources/application.yml b/accountant/accountant-app/src/main/resources/application.yml
index 25ba3928d..1f08d8860 100644
--- a/accountant/accountant-app/src/main/resources/application.yml
+++ b/accountant/accountant-app/src/main/resources/application.yml
@@ -16,12 +16,13 @@ spring:
password: ${dbpassword:hiopex}
initialization-mode: always
pool:
- #max-idle-time: 5m
- #max-life-time: 5m
- #max-acquire-time: 5m
- initial-size: 10
+ initial-size: 5
max-size: 32
- validation-query: select 1
+ max-idle-time: 10m
+ max-life-time: 30m
+ max-acquire-time: 10s
+ max-create-connection-time: 10s
+ validation-query: SELECT 1
cloud:
bootstrap:
enabled: true
@@ -103,4 +104,22 @@ app:
retry:
count: 10
delay-seconds: 5
- delay-multiplier: 3
\ No newline at end of file
+ delay-multiplier: 3
+ zone-offset: +03:30
+ trade-volume-calculation-currency: ${TRADE_VOLUME_CALCULATION_CURRENCY:USDT}
+ withdraw-volume-calculation-currency: ${WITHDRAW_VOLUME_CALCULATION_CURRENCY:USDT}
+ deposit-volume-calculation-currency: ${DEPOSIT_VOLUME_CALCULATION_CURRENCY:USDT}
+ custom-message:
+ enabled: ${CUSTOM_MESSAGE_ENABLED:false}
+ base-url: ${CUSTOM_MESSAGE_URL}
+ custom-user-language:
+ enabled: ${CUSTOM_USER_LANGUAGE_ENABLED:false}
+
+
+# --- Swagger / SpringDoc (env-driven) ---
+springdoc:
+ api-docs:
+ enabled: ${SWAGGER_API_DOCS_ENABLED:false}
+ swagger-ui:
+ enabled: ${SWAGGER_UI_ENABLED:false}
+ path: ${SWAGGER_UI_PATH:/swagger-ui.html}
diff --git a/accountant/accountant-app/src/test/resources/application.yml b/accountant/accountant-app/src/test/resources/application.yml
index 005d0e363..f495cb20e 100644
--- a/accountant/accountant-app/src/test/resources/application.yml
+++ b/accountant/accountant-app/src/test/resources/application.yml
@@ -38,7 +38,7 @@ management:
web:
base-path: /actuator
exposure:
- include: ["health", "metrics"]
+ include: [ "health", "metrics" ]
endpoint:
health:
show-details: always
@@ -54,4 +54,8 @@ app:
retry:
count: 10
delay-seconds: 5
- delay-multiplier: 3
\ No newline at end of file
+ delay-multiplier: 3
+ zone-offset: +03:30
+ trade-volume-calculation-currency: USDT
+ withdraw-volume-calculation-currency: USDT
+ deposit-volume-calculation-currency: USDT
\ No newline at end of file
diff --git a/accountant/accountant-core/pom.xml b/accountant/accountant-core/pom.xml
index fe9705d0c..9e689f4d0 100644
--- a/accountant/accountant-core/pom.xml
+++ b/accountant/accountant-core/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -66,5 +66,11 @@
jackson-datatype-jsr310
2.15.2
+
+ com.sun.mail
+ mailapi
+ 1.6.2
+ compile
+
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/DepositActivityManager.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/DepositActivityManager.kt
new file mode 100644
index 000000000..b655d77ac
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/DepositActivityManager.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.accountant.core.api
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+
+interface DepositActivityManager {
+ suspend fun getLastDaysDepositActivity(
+ userId: String,
+ quoteCurrency: String? = null,
+ n: Int = 31
+ ): List
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt
index 97adf6933..205bf336b 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt
@@ -3,10 +3,13 @@ package co.nilin.opex.accountant.core.api
import co.nilin.opex.accountant.core.model.FeeFinancialActions
import co.nilin.opex.accountant.core.model.FinancialAction
import co.nilin.opex.accountant.core.model.Order
+import co.nilin.opex.accountant.core.model.UserFee
import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent
interface FeeCalculator {
+ suspend fun getUserFee(uuid: String): UserFee
+
suspend fun createFeeActions(
trade: TradeEvent,
makerOrder: Order,
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/TradeActivityManager.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/TradeActivityManager.kt
new file mode 100644
index 000000000..c19491256
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/TradeActivityManager.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.accountant.core.api
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+
+interface TradeActivityManager {
+ suspend fun getLastDaysTradeActivity(
+ userId: String,
+ quoteCurrency: String? = null,
+ n: Int = 31
+ ): List
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/WithdrawActivityManager.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/WithdrawActivityManager.kt
new file mode 100644
index 000000000..c21b4eb56
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/WithdrawActivityManager.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.accountant.core.api
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+
+interface WithdrawActivityManager {
+ suspend fun getLastDaysWithdrawActivity(
+ userId: String,
+ quoteCurrency: String? = null,
+ n: Int = 31
+ ): List
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/CurrencyPrice.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/CurrencyPrice.kt
new file mode 100644
index 000000000..35308b1eb
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/CurrencyPrice.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.accountant.core.model
+
+import java.math.BigDecimal
+
+data class CurrencyPrice(var currency: String, val buyPrice: BigDecimal? = null, var sellPrice: BigDecimal? = null)
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/DailyAmount.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/DailyAmount.kt
new file mode 100644
index 000000000..c30d8c689
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/DailyAmount.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.accountant.core.model
+
+import java.math.BigDecimal
+import java.time.LocalDate
+
+data class DailyAmount(
+ val date: LocalDate,
+ val totalAmount: BigDecimal
+)
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt
new file mode 100644
index 000000000..00df04871
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.accountant.core.model
+
+import java.math.BigDecimal
+
+data class FeeConfig(
+ val name: String,
+ val displayOrder: Int,
+ val minAssetVolume: BigDecimal,
+ val maxAssetVolume: BigDecimal? = null,
+ val minTradeVolume: BigDecimal,
+ val maxTradeVolume: BigDecimal? = null,
+ val makerFee: BigDecimal,
+ val takerFee: BigDecimal,
+ val condition: Condition
+)
+enum class Condition { AND, OR }
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/KycLevel.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/KycLevel.kt
index dc73dfc2b..c68bb84ed 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/KycLevel.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/KycLevel.kt
@@ -1,6 +1,6 @@
package co.nilin.opex.accountant.core.model
enum class KycLevel {
- Level1, Level2,
+ LEVEL_1, LEVEL_2,LEVEL_3
}
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt
deleted file mode 100644
index 052e6f2f8..000000000
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package co.nilin.opex.accountant.core.model
-
-import java.math.BigDecimal
-
-class PairFeeConfig(
- val pairConfig: PairConfig,
- val direction: String,
- val userLevel: String,
- val makerFee: BigDecimal,
- val takerFee: BigDecimal
-)
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt
new file mode 100644
index 000000000..8867165f9
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.accountant.core.model
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class TotalAssetsSnapshot(
+ val uuid: String,
+ val totalAmount: BigDecimal,
+ val quoteCurrency: String,
+ val snapshotDate: LocalDateTime,
+)
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt
new file mode 100644
index 000000000..f6c0c7e7b
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.accountant.core.model
+
+import java.math.BigDecimal
+
+data class UserFee(
+ val name: String,
+ val makerFee: BigDecimal,
+ val takerFee: BigDecimal,
+)
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/WithdrawLimitConfig.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/WithdrawLimitConfig.kt
new file mode 100644
index 000000000..27ec36230
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/WithdrawLimitConfig.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.accountant.core.model
+
+import java.math.BigDecimal
+
+data class WithdrawLimitConfig(
+ val name: String,
+ val dailyMaxAmount: BigDecimal,
+)
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/WithdrawStatus.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/WithdrawStatus.kt
new file mode 100644
index 000000000..f8ebdaf2c
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/WithdrawStatus.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.accountant.core.model
+
+enum class WithdrawStatus {
+ CREATED,
+ PROCESSING,
+ CANCELED,
+ REJECTED,
+ DONE
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/DepositActivityManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/DepositActivityManagerImpl.kt
new file mode 100644
index 000000000..22c5fa720
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/DepositActivityManagerImpl.kt
@@ -0,0 +1,73 @@
+package co.nilin.opex.accountant.core.service
+
+import co.nilin.opex.accountant.core.api.DepositActivityManager
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.spi.UserDepositVolumePersister
+import co.nilin.opex.common.utils.CacheManager
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.ZoneOffset
+import java.util.concurrent.TimeUnit
+import java.util.stream.Collectors
+
+@Service
+class DepositActivityManagerImpl(
+ private val cacheManager: CacheManager,
+ private val depositVolumePersister: UserDepositVolumePersister,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+) : DepositActivityManager {
+ override suspend fun getLastDaysDepositActivity(
+ userId: String,
+ quoteCurrency: String?,
+ n: Int
+ ): List {
+ val today = LocalDate.now(ZoneOffset.of(zoneOffsetString))
+ val dates = (0..n - 1).map { today.minusDays(it.toLong()) }
+
+ val result = mutableMapOf()
+ val missingDates = mutableListOf()
+
+ for (date in dates) {
+ val cacheKey = "deposit:daily:$userId:$date"
+ val cached = cacheManager.get(cacheKey)
+
+ if (cached != null) {
+ result[date] = cached
+ } else {
+ missingDates.add(date)
+ }
+ }
+
+ if (missingDates.isNotEmpty()) {
+ val startDate = missingDates.minOrNull()!!
+
+ val dbData = depositVolumePersister.getLastDaysDeposit(userId, startDate, quoteCurrency)
+ .stream().collect(Collectors.toMap(DailyAmount::date, DailyAmount::totalAmount));
+
+ for (date in missingDates) {
+ val value = dbData[date] ?: BigDecimal.ZERO
+ val (ttl, unit) = ttlFor(date, today)
+ val cacheKey = "deposit:daily:$userId:$date"
+
+ cacheManager.put(cacheKey, value, ttl, unit)
+ result[date] = value
+ }
+ }
+
+ return result
+ .map { DailyAmount(it.key, it.value) }
+ .sortedBy { it.date }
+
+ }
+
+
+ private fun ttlFor(date: LocalDate, today: LocalDate): Pair =
+ if (date == today) {
+ 15L to TimeUnit.MINUTES
+ } else {
+ 100L to TimeUnit.DAYS
+ }
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt
index 7e22d75ab..789cc388f 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt
@@ -2,21 +2,59 @@ package co.nilin.opex.accountant.core.service
import co.nilin.opex.accountant.core.api.FeeCalculator
import co.nilin.opex.accountant.core.model.*
+import co.nilin.opex.accountant.core.spi.FeeConfigService
import co.nilin.opex.accountant.core.spi.JsonMapper
+import co.nilin.opex.accountant.core.spi.UserTradeVolumePersister
+import co.nilin.opex.accountant.core.spi.WalletProxy
+import co.nilin.opex.common.utils.CacheManager
import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent
import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
+import java.math.BigDecimal
+import java.time.Duration
import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.util.concurrent.TimeUnit
@Component
class FeeCalculatorImpl(
+ private val walletProxy: WalletProxy,
+ private val feeConfigService: FeeConfigService,
+ private val userVolumePersister: UserTradeVolumePersister,
+ @Qualifier("appCacheManager") private val cacheManager: CacheManager,
@Value("\${app.address}") private val platformAddress: String,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+ @Value("\${app.trade-volume-calculation-currency}")
+ private val tradeVolumeCalculationCurrency: String,
private val jsonMapper: JsonMapper
) : FeeCalculator {
private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java)
+ override suspend fun getUserFee(uuid: String): UserFee {
+ val cached = cacheManager.get(uuid)
+ if (cached != null) {
+ return cached
+ }
+
+ val totalAssets = walletProxy.getUserTotalAssets(uuid)
+ val userVolumeData = userVolumePersister.getUserTotalTradeVolume(
+ uuid,
+ LocalDateTime.now().atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate().minusMonths(1L),
+ tradeVolumeCalculationCurrency
+ )
+
+ val feeConfig = feeConfigService.loadMatchingFeeConfig(
+ totalAssets?.totalAmount ?: BigDecimal.ZERO,
+ userVolumeData ?: BigDecimal.ZERO
+ )
+ val remainingMillis = remainingMillisUntil1AM()
+ cacheManager.put(uuid, feeConfig, remainingMillis, TimeUnit.MILLISECONDS)
+ return feeConfig
+ }
+
override suspend fun createFeeActions(
trade: TradeEvent,
makerOrder: Order,
@@ -84,4 +122,12 @@ class FeeCalculatorImpl(
val eventMap: Map = jsonMapper.toMap(tradeEvent)
return orderMap + eventMap
}
+
+ private fun remainingMillisUntil1AM(): Long {
+ val now = LocalDateTime.now().atOffset(ZoneOffset.of(zoneOffsetString))
+ val target =
+ LocalDateTime.now().atOffset(ZoneOffset.of(zoneOffsetString)).plusDays(1).withHour(1)
+ val remainingMillis = Duration.between(now, target).toMillis()
+ return if (remainingMillis > 0) remainingMillis else Duration.ofHours(1).toMillis()
+ }
}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt
index f6c25d076..0dce40930 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt
@@ -3,7 +3,9 @@ package co.nilin.opex.accountant.core.service
import co.nilin.opex.accountant.core.api.FinancialActionJobManager
import co.nilin.opex.accountant.core.model.FinancialAction
import co.nilin.opex.accountant.core.model.FinancialActionStatus
-import co.nilin.opex.accountant.core.spi.*
+import co.nilin.opex.accountant.core.spi.FinancialActionLoader
+import co.nilin.opex.accountant.core.spi.FinancialActionPersister
+import co.nilin.opex.accountant.core.spi.WalletProxy
import co.nilin.opex.utility.error.data.OpexException
import org.slf4j.LoggerFactory
import org.springframework.web.reactive.function.client.WebClientResponseException
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt
index d4bfe0b42..34bf51f4b 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt
@@ -1,5 +1,6 @@
package co.nilin.opex.accountant.core.service
+import co.nilin.opex.accountant.core.api.FeeCalculator
import co.nilin.opex.accountant.core.api.OrderManager
import co.nilin.opex.accountant.core.inout.OrderStatus
import co.nilin.opex.accountant.core.inout.RichOrder
@@ -15,14 +16,13 @@ import java.time.LocalDateTime
open class OrderManagerImpl(
private val pairConfigLoader: PairConfigLoader,
- private val userLevelLoader: UserLevelLoader,
private val financialActionPersister: FinancialActionPersister,
private val financeActionLoader: FinancialActionLoader,
private val orderPersister: OrderPersister,
private val tempEventPersister: TempEventPersister,
private val richOrderPublisher: RichOrderPublisher,
private val financialActionPublisher: FinancialActionPublisher,
- private val jsonMapper: JsonMapper,
+ private val feeCalculator: FeeCalculator
) : OrderManager {
@Transactional
@@ -36,14 +36,13 @@ open class OrderManagerImpl(
submitOrderEvent.pair.rightSideName
}
- val level = userLevelLoader.load(submitOrderEvent.uuid)
- val pairFeeConfig = pairConfigLoader.load(
+ val pairConfig = pairConfigLoader.load(
submitOrderEvent.pair.toString(),
submitOrderEvent.direction,
- level
)
- val makerFee = pairFeeConfig.makerFee * BigDecimal.ONE //user level formula
- val takerFee = pairFeeConfig.takerFee * BigDecimal.ONE //user level formula
+ val fee = feeCalculator.getUserFee(submitOrderEvent.uuid)
+ val makerFee = fee.makerFee
+ val takerFee = fee.takerFee
//create fa for transfer uuid symbol main wallet to uuid symbol exchange wallet
/*
@@ -52,11 +51,11 @@ open class OrderManagerImpl(
*/
val amount = if (submitOrderEvent.direction == OrderDirection.ASK) {
- BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction)
+ BigDecimal(submitOrderEvent.quantity).multiply(pairConfig.leftSideFraction)
} else {
- BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction)
+ BigDecimal(submitOrderEvent.quantity).multiply(pairConfig.leftSideFraction)
.multiply(submitOrderEvent.price.toBigDecimal())
- .multiply(pairFeeConfig.pairConfig.rightSideFraction)
+ .multiply(pairConfig.rightSideFraction)
}
//store order (ouid, uuid, fees, userlevel, pair, direction, price, quantity, filledQ, status, transfered)
@@ -67,8 +66,8 @@ open class OrderManagerImpl(
null,
makerFee,
takerFee,
- pairFeeConfig.pairConfig.leftSideFraction,
- pairFeeConfig.pairConfig.rightSideFraction,
+ pairConfig.leftSideFraction,
+ pairConfig.rightSideFraction,
submitOrderEvent.uuid,
submitOrderEvent.userLevel,
submitOrderEvent.direction,
@@ -78,10 +77,10 @@ open class OrderManagerImpl(
submitOrderEvent.quantity,
submitOrderEvent.quantity - submitOrderEvent.remainedQuantity,
submitOrderEvent.price.toBigDecimal()
- .multiply(pairFeeConfig.pairConfig.rightSideFraction),
+ .multiply(pairConfig.rightSideFraction),
submitOrderEvent.quantity.toBigDecimal()
- .multiply(pairFeeConfig.pairConfig.leftSideFraction),
- BigDecimal(submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).multiply(pairFeeConfig.pairConfig.leftSideFraction),
+ .multiply(pairConfig.leftSideFraction),
+ BigDecimal(submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).multiply(pairConfig.leftSideFraction),
amount,
amount,
OrderStatus.REQUESTED.code
@@ -269,22 +268,10 @@ open class OrderManagerImpl(
}
}
- private fun createMap(rejectOrderEvent: RejectOrderEvent, order: Order): Map {
- val orderMap: Map = jsonMapper.toMap(order)
- val eventMap: Map = jsonMapper.toMap(rejectOrderEvent)
- return orderMap + eventMap
- }
+ private suspend fun getFee(uuid: String): BigDecimal {
- private fun createMap(cancelOrderEvent: CancelOrderEvent, order: Order): Map {
- val orderMap: Map = jsonMapper.toMap(order)
- val eventMap: Map = jsonMapper.toMap(cancelOrderEvent)
- return orderMap + eventMap
- }
- private fun createMap(submitOrderEvent: SubmitOrderEvent, order: Order): Map {
- val orderMap: Map = jsonMapper.toMap(order)
- val eventMap: Map = jsonMapper.toMap(submitOrderEvent)
- return orderMap + eventMap
+ return BigDecimal.ONE
}
}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeActivityManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeActivityManagerImpl.kt
new file mode 100644
index 000000000..51ed4dbd8
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeActivityManagerImpl.kt
@@ -0,0 +1,73 @@
+package co.nilin.opex.accountant.core.service
+
+import co.nilin.opex.accountant.core.api.TradeActivityManager
+import co.nilin.opex.accountant.core.api.WithdrawActivityManager
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.spi.UserTradeVolumePersister
+import co.nilin.opex.common.utils.CacheManager
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.ZoneOffset
+import java.util.concurrent.TimeUnit
+import java.util.stream.Collectors
+@Service
+class TradeActivityManagerImpl(
+ private val cacheManager: CacheManager,
+ private val tradeVolumePersister: UserTradeVolumePersister,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+) : TradeActivityManager {
+ override suspend fun getLastDaysTradeActivity(
+ userId: String,
+ quoteCurrency: String?,
+ n: Int
+ ): List {
+ val today = LocalDate.now(ZoneOffset.of(zoneOffsetString))
+ val dates = (0..n - 1).map { today.minusDays(it.toLong()) }
+
+ val result = mutableMapOf()
+ val missingDates = mutableListOf()
+
+ for (date in dates) {
+ val cacheKey = "trade:daily:$userId:$date"
+ val cached = cacheManager.get(cacheKey)
+
+ if (cached != null) {
+ result[date] = cached
+ } else {
+ missingDates.add(date)
+ }
+ }
+
+ if (missingDates.isNotEmpty()) {
+ val startDate = missingDates.minOrNull()!!
+
+ val dbData = tradeVolumePersister.getLastDaysTrade(userId, startDate,quoteCurrency)
+ .stream().collect(Collectors.toMap(DailyAmount::date, DailyAmount::totalAmount));
+
+ for (date in missingDates) {
+ val value = dbData[date] ?: BigDecimal.ZERO
+ val (ttl, unit) = ttlFor(date, today)
+ val cacheKey = "trade:daily:$userId:$date"
+
+ cacheManager.put(cacheKey, value, ttl, unit)
+ result[date] = value
+ }
+ }
+
+ return result
+ .map { DailyAmount(it.key, it.value) }
+ .sortedBy { it.date }
+
+ }
+
+
+ private fun ttlFor(date: LocalDate, today: LocalDate): Pair =
+ if (date == today) {
+ 15L to TimeUnit.MINUTES
+ } else {
+ 100L to TimeUnit.DAYS
+ }
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt
index b6c574ea3..f6ce24662 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDateTime
+import java.time.ZoneOffset
open class TradeManagerImpl(
private val financeActionPersister: FinancialActionPersister,
@@ -22,7 +23,10 @@ open class TradeManagerImpl(
private val richOrderPublisher: RichOrderPublisher,
private val feeCalculator: FeeCalculator,
private val financialActionPublisher: FinancialActionPublisher,
- private val jsonMapper: JsonMapper
+ private val currencyRatePersister: CurrencyRatePersister,
+ private val userVolumePersister: UserTradeVolumePersister,
+ private val tradeVolumeCalculationCurrency: String,
+ private val zoneOffsetString: String,
) : TradeManager {
private val logger = LoggerFactory.getLogger(TradeManagerImpl::class.java)
@@ -154,39 +158,71 @@ open class TradeManagerImpl(
val takerPrice = trade.takerPrice.toBigDecimal().multiply(takerOrder.rightSideFraction)
val makerPrice = trade.makerPrice.toBigDecimal().multiply(makerOrder.rightSideFraction)
- richTradePublisher.publish(
- RichTrade(
- trade.tradeId,
- trade.pair.toString(),
- trade.takerOuid,
- trade.takerUuid,
- trade.takerOrderId,
- trade.takerDirection,
- takerPrice,
- takerOrder.origQuantity,
- takerOrder.origPrice.multiply(takerOrder.origQuantity),
- trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction),
- feeActions.takerFeeAction.amount,
- feeActions.takerFeeAction.symbol,
- trade.makerOuid,
- trade.makerUuid,
- trade.makerOrderId,
- trade.makerDirection,
- makerPrice,
- makerOrder.origQuantity,
- makerOrder.origPrice.multiply(makerOrder.origQuantity),
- trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction),
- feeActions.makerFeeAction.amount,
- feeActions.makerFeeAction.symbol,
- makerPrice,
- trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction),
- trade.eventDate
- )
+ val richTrade = RichTrade(
+ trade.tradeId,
+ trade.pair.toString(),
+ trade.takerOuid,
+ trade.takerUuid,
+ trade.takerOrderId,
+ trade.takerDirection,
+ takerPrice,
+ takerOrder.origQuantity,
+ takerOrder.origPrice.multiply(takerOrder.origQuantity),
+ trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction),
+ feeActions.takerFeeAction.amount,
+ feeActions.takerFeeAction.symbol,
+ trade.makerOuid,
+ trade.makerUuid,
+ trade.makerOrderId,
+ trade.makerDirection,
+ makerPrice,
+ makerOrder.origQuantity,
+ makerOrder.origPrice.multiply(makerOrder.origQuantity),
+ trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction),
+ feeActions.makerFeeAction.amount,
+ feeActions.makerFeeAction.symbol,
+ makerPrice,
+ trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction),
+ trade.eventDate
)
+ richTradePublisher.publish(richTrade)
+
+ currencyRatePersister.updateRate(trade.pair.leftSideName, trade.pair.rightSideName, makerPrice)
+ calculateTradeVolume(richTrade, trade.pair.leftSideName, trade.pair.rightSideName)
+
return financeActionPersister.persist(financialActions)
//return financeActionPersister.persist(financialActions).also { publishFinancialActions(it) }
}
+ private suspend fun calculateTradeVolume(trade: RichTrade, base: String, quote: String) {
+ val today = LocalDateTime.now().atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate()
+
+ val rate = if (quote == tradeVolumeCalculationCurrency) {
+ currencyRatePersister.getRate(base, quote)
+ } else {
+ currencyRatePersister.getRate(quote, tradeVolumeCalculationCurrency)
+ }
+ val totalAmount = trade.matchedQuantity.multiply(rate)
+
+ userVolumePersister.update(
+ trade.makerUuid,
+ base,
+ today,
+ trade.matchedQuantity,
+ totalAmount,
+ tradeVolumeCalculationCurrency
+ )
+ userVolumePersister.update(
+ trade.takerUuid,
+ base,
+ today,
+ trade.matchedQuantity,
+ totalAmount,
+ tradeVolumeCalculationCurrency
+ )
+ logger.info("Trade volume updated")
+ }
+
private fun createFinalizeOrderFinancialAction(
parentTransferAction: FinancialAction?,
order: Order,
@@ -250,10 +286,4 @@ open class TradeManagerImpl(
if (!list.contains(financialAction))
list.add(financialAction)
}
-
- private fun createMap(tradeEvent: TradeEvent, order: Order): Map {
- val orderMap: Map = jsonMapper.toMap(order)
- val eventMap: Map = jsonMapper.toMap(tradeEvent)
- return orderMap + eventMap
- }
}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/WithdrawActivityManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/WithdrawActivityManagerImpl.kt
new file mode 100644
index 000000000..cbf8dd491
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/WithdrawActivityManagerImpl.kt
@@ -0,0 +1,72 @@
+package co.nilin.opex.accountant.core.service
+
+import co.nilin.opex.accountant.core.api.WithdrawActivityManager
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.spi.UserWithdrawVolumePersister
+import co.nilin.opex.common.utils.CacheManager
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.ZoneOffset
+import java.util.concurrent.TimeUnit
+import java.util.stream.Collectors
+@Service
+class WithdrawActivityManagerImpl(
+ private val cacheManager: CacheManager,
+ private val withdrawVolumePersister: UserWithdrawVolumePersister,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+) : WithdrawActivityManager {
+ override suspend fun getLastDaysWithdrawActivity(
+ userId: String,
+ quoteCurrency: String?,
+ n: Int
+ ): List {
+ val today = LocalDate.now(ZoneOffset.of(zoneOffsetString))
+ val dates = (0..n - 1).map { today.minusDays(it.toLong()) }
+
+ val result = mutableMapOf()
+ val missingDates = mutableListOf()
+
+ for (date in dates) {
+ val cacheKey = "withdraw:daily:$userId:$date"
+ val cached = cacheManager.get(cacheKey)
+
+ if (cached != null) {
+ result[date] = cached
+ } else {
+ missingDates.add(date)
+ }
+ }
+
+ if (missingDates.isNotEmpty()) {
+ val startDate = missingDates.minOrNull()!!
+
+ val dbData = withdrawVolumePersister.getLastDaysWithdraw(userId, startDate,quoteCurrency)
+ .stream().collect(Collectors.toMap(DailyAmount::date, DailyAmount::totalAmount));
+
+ for (date in missingDates) {
+ val value = dbData[date] ?: BigDecimal.ZERO
+ val (ttl, unit) = ttlFor(date, today)
+ val cacheKey = "withdraw:daily:$userId:$date"
+
+ cacheManager.put(cacheKey, value, ttl, unit)
+ result[date] = value
+ }
+ }
+
+ return result
+ .map { DailyAmount(it.key, it.value) }
+ .sortedBy { it.date }
+
+ }
+
+
+ private fun ttlFor(date: LocalDate, today: LocalDate): Pair =
+ if (date == today) {
+ 15L to TimeUnit.MINUTES
+ } else {
+ 100L to TimeUnit.DAYS
+ }
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt
new file mode 100644
index 000000000..96ded5fde
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.accountant.core.spi
+
+import java.math.BigDecimal
+
+interface CurrencyRatePersister {
+
+ suspend fun updateRate(base: String, quote: String, rate: BigDecimal)
+
+ suspend fun getRate(base: String, quote: String): BigDecimal
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt
new file mode 100644
index 000000000..e258cd7c9
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.accountant.core.spi
+
+import co.nilin.opex.accountant.core.model.FeeConfig
+import co.nilin.opex.accountant.core.model.UserFee
+import java.math.BigDecimal
+
+interface FeeConfigService {
+
+ suspend fun loadFeeConfigs(): List
+ suspend fun saveFee(config: FeeConfig)
+ suspend fun updateFee(config: FeeConfig)
+ suspend fun loadFeeConfig(name: String): FeeConfig
+ suspend fun loadMatchingFeeConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): UserFee
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt
index 800a8fe4b..a6a1d1c67 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt
@@ -1,20 +1,11 @@
package co.nilin.opex.accountant.core.spi
import co.nilin.opex.accountant.core.model.PairConfig
-import co.nilin.opex.accountant.core.model.PairFeeConfig
import co.nilin.opex.matching.engine.core.model.OrderDirection
interface PairConfigLoader {
suspend fun loadPairConfigs(): List
- suspend fun loadPairFeeConfigs(): List
-
- suspend fun loadPairFeeConfigs(direction: OrderDirection, userLevel: String): List
-
- suspend fun loadPairFeeConfigs(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig?
-
- suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig
-
suspend fun load(pair: String, direction: OrderDirection): PairConfig
}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserDepositVolumePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserDepositVolumePersister.kt
new file mode 100644
index 000000000..496ccf3a4
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserDepositVolumePersister.kt
@@ -0,0 +1,20 @@
+package co.nilin.opex.accountant.core.spi
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.model.WithdrawStatus
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+interface UserDepositVolumePersister {
+ suspend fun update(
+ userId: String,
+ currency: String,
+ amount: BigDecimal,
+ date: LocalDateTime)
+
+ suspend fun getTotalValueByUserAndDateAfter(uuid: String, startDate: LocalDateTime): BigDecimal
+
+ suspend fun getLastDaysDeposit(userId: String, startDate: LocalDate?, quatCurrency: String?, lastDays: Long = 31): List
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt
deleted file mode 100644
index b1e2c3173..000000000
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package co.nilin.opex.accountant.core.spi
-
-import co.nilin.opex.accountant.core.model.KycLevel
-
-interface UserLevelLoader {
-
- suspend fun load(uuid: String): String
-
- suspend fun update(uuid: String,userLevel:KycLevel)
-
-}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserTradeVolumePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserTradeVolumePersister.kt
new file mode 100644
index 000000000..5e6de6bbe
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserTradeVolumePersister.kt
@@ -0,0 +1,27 @@
+package co.nilin.opex.accountant.core.spi
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import java.math.BigDecimal
+import java.time.LocalDate
+
+interface UserTradeVolumePersister {
+
+ suspend fun update(
+ userId: String,
+ currency: String,
+ date: LocalDate,
+ volume: BigDecimal,
+ totalAmount: BigDecimal,
+ quoteCurrency: String
+ )
+
+ suspend fun getUserTotalTradeVolume(uuid: String, startDate: LocalDate, quoteCurrency: String): BigDecimal?
+ suspend fun getUserTotalTradeVolumeByCurrency(
+ uuid: String,
+ currency: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): BigDecimal?
+ suspend fun getLastDaysTrade(userId: String, startDate: LocalDate?, quatCurrency: String?, lastDays: Long = 31): List
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserWithdrawVolumePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserWithdrawVolumePersister.kt
new file mode 100644
index 000000000..7bc056c49
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserWithdrawVolumePersister.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.accountant.core.spi
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.model.WithdrawStatus
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+interface UserWithdrawVolumePersister {
+ suspend fun update(
+ userId: String, currency: String, amount: BigDecimal, date: LocalDateTime, withdrawStatus: WithdrawStatus
+ )
+
+ suspend fun getTotalValueByUserAndDateAfter(uuid: String, startDate: LocalDateTime): BigDecimal
+
+ suspend fun getLastDaysWithdraw(userId: String, startDate: LocalDate?, quatCurrency: String?, lastDays: Long = 31): List
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WalletProxy.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WalletProxy.kt
index 59bdce3fe..78575b25d 100644
--- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WalletProxy.kt
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WalletProxy.kt
@@ -1,5 +1,7 @@
package co.nilin.opex.accountant.core.spi
+import co.nilin.opex.accountant.core.model.CurrencyPrice
+import co.nilin.opex.accountant.core.model.TotalAssetsSnapshot
import co.nilin.opex.accountant.core.model.WalletType
import java.math.BigDecimal
@@ -18,4 +20,10 @@ interface WalletProxy {
)
suspend fun canFulfil(symbol: String, walletType: WalletType, uuid: String, amount: BigDecimal): Boolean
+
+ suspend fun getUserTotalAssets(
+ uuid: String
+ ): TotalAssetsSnapshot?
+
+ suspend fun getPrices(quote : String) : List
}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WithdrawLimitManager.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WithdrawLimitManager.kt
new file mode 100644
index 000000000..be40bf526
--- /dev/null
+++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WithdrawLimitManager.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.accountant.core.spi
+
+import co.nilin.opex.accountant.core.model.WithdrawLimitConfig
+import java.math.BigDecimal
+
+interface WithdrawLimitManager {
+ suspend fun canWithdraw(uuid: String, userLevel: String, currency: String, amount: BigDecimal): Boolean
+ suspend fun getAll(): List
+}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt
index 8add7dac4..55e69d80d 100644
--- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt
+++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt
@@ -2,16 +2,40 @@ package co.nilin.opex.accountant.core.service
import co.nilin.opex.accountant.core.model.FinancialAction
import co.nilin.opex.accountant.core.model.FinancialActionCategory
+import co.nilin.opex.accountant.core.model.UserFee
import co.nilin.opex.accountant.core.model.WalletType
+import co.nilin.opex.accountant.core.spi.FeeConfigService
+import co.nilin.opex.accountant.core.spi.UserTradeVolumePersister
+import co.nilin.opex.accountant.core.spi.WalletProxy
+import co.nilin.opex.common.utils.CacheManager
import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent
+import io.mockk.*
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Qualifier
+import java.math.BigDecimal
+import java.util.concurrent.TimeUnit
+
internal class FeeCalculatorImplTest {
+ private val walletProxy = mockk()
+ private val feeConfigService = mockk()
+ private val userVolumePersister = mockk()
+ @Qualifier("appCacheManager") private val cacheManager = mockk>()
+
private val receiverAddress = "0x0"
- private val feeCalculator = FeeCalculatorImpl(receiverAddress, JsonMapperTestImpl())
+ private val feeCalculator = FeeCalculatorImpl(
+ walletProxy,
+ feeConfigService,
+ userVolumePersister,
+ cacheManager,
+ receiverAddress,
+ "+03:30",
+ "USDT",
+ JsonMapperTestImpl(),
+ )
@Test
fun givenTradeEventsAndOrders_whenFeeCalculated_feeActionsNotNull(): Unit = runBlocking {
@@ -71,4 +95,70 @@ internal class FeeCalculatorImplTest {
assertThat(actions.takerFeeAction.parent).isNotNull
}
+ @Test
+ fun givenCachedValueExists_whenGetUserFeeCalled_thenReturnCachedValueAndSkipServices() {
+ runBlocking {
+ val uuid = "user_1"
+ val cachedFee = UserFee("test1", BigDecimal("0.1"), BigDecimal("0.1"))
+
+ every { cacheManager.get(uuid) } returns cachedFee
+
+ val result = feeCalculator.getUserFee(uuid)
+
+ assertThat(result).isEqualTo(cachedFee)
+ verify(exactly = 1) { cacheManager.get(uuid) }
+ verify { walletProxy wasNot Called }
+ verify { userVolumePersister wasNot Called }
+ verify { feeConfigService wasNot Called }
+ }
+ }
+
+ @Test
+ fun givenNoCachedValue_whenGetUserFee_thenFetchFromServicesAndCacheIt() = runBlocking {
+ val uuid = "user_2"
+ val expectedFee = UserFee("test2", BigDecimal("0.2"), BigDecimal("0.2"))
+
+ every { cacheManager.get(uuid) } returns null
+ coEvery { walletProxy.getUserTotalAssets(uuid) } returns mockk {
+ every { totalAmount } returns BigDecimal("1000")
+ }
+ coEvery { userVolumePersister.getUserTotalTradeVolume(any(), any(), any()) } returns BigDecimal("200")
+ coEvery { feeConfigService.loadMatchingFeeConfig(any(), any()) } returns expectedFee
+ every { cacheManager.put(eq(uuid), eq(expectedFee), any(), eq(TimeUnit.MILLISECONDS)) } just Runs
+
+ val result = feeCalculator.getUserFee(uuid)
+
+ assertThat(result).isEqualTo(expectedFee)
+ coVerifyOrder {
+ cacheManager.get(uuid)
+ walletProxy.getUserTotalAssets(uuid)
+ }
+ coVerifyOrder {
+ userVolumePersister.getUserTotalTradeVolume(any(), any(), any())
+ }
+ coVerifyOrder {
+ feeConfigService.loadMatchingFeeConfig(BigDecimal("1000"), BigDecimal("200"))
+ cacheManager.put(uuid, expectedFee, any(), TimeUnit.MILLISECONDS)
+ }
+ }
+
+ @Test
+ fun givenAssetsAndVolume_whenGetUserFee_thenPassCorrectValuesToFeeConfigService() = runBlocking {
+ val uuid = "user_3"
+ val assets = BigDecimal("5000")
+ val volume = BigDecimal("300")
+ val expectedFee = UserFee("test", BigDecimal("0.1"), BigDecimal("0.1"))
+
+ every { cacheManager.get(uuid) } returns null
+ coEvery { walletProxy.getUserTotalAssets(uuid) } returns mockk { every { totalAmount } returns assets }
+ coEvery { userVolumePersister.getUserTotalTradeVolume(eq(uuid), any(), eq("USDT")) } returns volume
+ coEvery { feeConfigService.loadMatchingFeeConfig(assets, volume) } returns expectedFee
+ every { cacheManager.put(eq(uuid), eq(expectedFee), any(), eq(TimeUnit.MILLISECONDS)) } just Runs
+
+ val result = feeCalculator.getUserFee(uuid)
+
+ assertThat(result).isEqualTo(expectedFee)
+ coVerify { feeConfigService.loadMatchingFeeConfig(assets, volume) }
+ }
+
}
\ No newline at end of file
diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt
index 24d1c05d6..6147be7e2 100644
--- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt
+++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt
@@ -1,5 +1,6 @@
package co.nilin.opex.accountant.core.service
+import co.nilin.opex.accountant.core.api.FeeCalculator
import co.nilin.opex.accountant.core.inout.OrderStatus
import co.nilin.opex.accountant.core.model.*
import co.nilin.opex.accountant.core.spi.*
@@ -30,19 +31,18 @@ internal class OrderManagerImplTest {
private val tempEventPersister = mockk()
private val pairConfigLoader = mockk()
private val richOrderPublisher = mockk()
- private val userLevelLoader = mockk()
private val financialActionPublisher = mockk()
+ private val feeCalculator = mockk()
private val orderManager = OrderManagerImpl(
pairConfigLoader,
- userLevelLoader,
financialActionPersister,
financialActionLoader,
orderPersister,
tempEventPersister,
richOrderPublisher,
financialActionPublisher,
- JsonMapperTestImpl()
+ feeCalculator
)
init {
@@ -54,7 +54,6 @@ internal class OrderManagerImplTest {
coEvery { financialActionPersister.persist(any()) } returnsArgument (0)
coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit
coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit
- coEvery { userLevelLoader.load(any()) } returns "*"
coEvery { financialActionPublisher.publish(any()) } returns Unit
}
@@ -73,18 +72,17 @@ internal class OrderManagerImplTest {
"ouid", "uuid", null, pair, 30, 60, 0, OrderDirection.ASK, MatchConstraint.GTC, OrderType.LIMIT_ORDER
)
- coEvery {
- pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any())
- } returns PairFeeConfig(
- pairConfig,
- submitOrderEvent.direction.toString(),
- "",
- BigDecimal.valueOf(0.1),
- BigDecimal.valueOf(0.12)
- )
-
coEvery { financialActionPersister.persist(any()) } returnsArgument (0)
+ coEvery {
+ pairConfigLoader.load(pair.toString(), submitOrderEvent.direction)
+ } returns pairConfig
+
+ coEvery {
+ feeCalculator.getUserFee(submitOrderEvent.uuid)
+ } returns UserFee(
+ "Test", BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)
+ )
//when
val financialActions = orderManager.handleRequestOrder(submitOrderEvent)
@@ -130,17 +128,17 @@ internal class OrderManagerImplTest {
"ouid", "uuid", null, pair, 35, 14, 0, OrderDirection.BID, MatchConstraint.GTC, OrderType.LIMIT_ORDER
)
+ coEvery { financialActionPersister.persist(any()) } returnsArgument (0)
+
coEvery {
- pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any())
- } returns PairFeeConfig(
- pairConfig,
- submitOrderEvent.direction.toString(),
- "",
- BigDecimal.valueOf(0.08),
- BigDecimal.valueOf(0.1)
- )
+ pairConfigLoader.load(pair.toString(), submitOrderEvent.direction)
+ } returns pairConfig
- coEvery { financialActionPersister.persist(any()) } returnsArgument (0)
+ coEvery {
+ feeCalculator.getUserFee(submitOrderEvent.uuid)
+ } returns UserFee(
+ "Test", BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)
+ )
//when
val financialActions = orderManager.handleRequestOrder(submitOrderEvent)
diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt
index ac5045208..455204684 100644
--- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt
+++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt
@@ -1,7 +1,9 @@
package co.nilin.opex.accountant.core.service
+import co.nilin.opex.accountant.core.api.FeeCalculator
import co.nilin.opex.accountant.core.model.*
import co.nilin.opex.accountant.core.spi.*
+import co.nilin.opex.common.utils.CacheManager
import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent
import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent
import co.nilin.opex.matching.engine.core.model.MatchConstraint
@@ -9,7 +11,9 @@ import co.nilin.opex.matching.engine.core.model.OrderDirection
import co.nilin.opex.matching.engine.core.model.OrderType
import co.nilin.opex.matching.engine.core.model.Pair
import io.mockk.coEvery
+import io.mockk.just
import io.mockk.mockk
+import io.mockk.runs
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
@@ -24,20 +28,25 @@ internal class TradeManagerImplTest {
private val tempEventPersister = mockk()
private val richOrderPublisher = mockk()
private val richTradePublisher = mockk()
- private val userLevelLoader = mockk()
private val financialActionPublisher = mockk()
+ private val currencyRatePersister = mockk()
+ private val userVolumePersister = mockk()
+ private val feeCalculator = mockk()
+ private val walletProxy = mockk()
+ private val feeConfigService = mockk()
+ private val cacheManager = mockk>()
+
private val jsonMapper = JsonMapperTestImpl()
private val orderManager = OrderManagerImpl(
pairConfigLoader,
- userLevelLoader,
financialActionPersister,
financeActionLoader,
orderPersister,
tempEventPersister,
richOrderPublisher,
financialActionPublisher,
- jsonMapper
+ feeCalculator
)
private val tradeManager = TradeManagerImpl(
@@ -47,9 +56,21 @@ internal class TradeManagerImplTest {
tempEventPersister,
richTradePublisher,
richOrderPublisher,
- FeeCalculatorImpl("0x0", jsonMapper),
+ FeeCalculatorImpl(
+ walletProxy,
+ feeConfigService,
+ userVolumePersister,
+ cacheManager,
+ "0x0",
+ "+03:30",
+ "USDT",
+ JsonMapperTestImpl()
+ ),
financialActionPublisher,
- jsonMapper
+ currencyRatePersister,
+ userVolumePersister,
+ "USDT",
+ "+03:30",
)
init {
@@ -58,10 +79,12 @@ internal class TradeManagerImplTest {
coEvery { financeActionLoader.findLast(any(), any()) } returns null
coEvery { richOrderPublisher.publish(any()) } returns Unit
coEvery { richTradePublisher.publish(any()) } returns Unit
- coEvery { userLevelLoader.load(any()) } returns "*"
coEvery { financialActionPublisher.publish(any()) } returns Unit
coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit
coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit
+ coEvery { currencyRatePersister.updateRate(any(), any(), any()) } just runs
+ coEvery { userVolumePersister.update(any(), any(), any(), any(), any(), any()) } just runs
+ coEvery { currencyRatePersister.getRate(any(), any()) } returns BigDecimal.ONE
}
@Test
@@ -309,22 +332,23 @@ internal class TradeManagerImplTest {
takerFee: BigDecimal
) {
coEvery {
- pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any())
- } returns PairFeeConfig(
- pairConfig,
- submitOrderEvent.direction.toString(),
- "",
- makerFee,
- takerFee
+ feeCalculator.getUserFee(submitOrderEvent.uuid)
+ } returns UserFee(
+ "Test", makerFee, takerFee
)
+
+ coEvery {
+ pairConfigLoader.load(pair.toString(), submitOrderEvent.direction)
+ } returns pairConfig
+
coEvery { financialActionPersister.persist(any()) } returnsArgument (0)
val financialActions = orderManager.handleRequestOrder(submitOrderEvent)
- val orderPairFeeConfig =
- pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "")
- val orderMakerFee = orderPairFeeConfig.makerFee * BigDecimal.ONE //user level formula
- val orderTakerFee = orderPairFeeConfig.takerFee * BigDecimal.ONE //user level formula
+ val pairConfig =
+ pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction)
+ val orderMakerFee = makerFee
+ val orderTakerFee = takerFee
coEvery { orderPersister.load(submitOrderEvent.ouid) } returns Order(
submitOrderEvent.pair.toString(),
@@ -332,8 +356,8 @@ internal class TradeManagerImplTest {
null,
orderMakerFee,
orderTakerFee,
- orderPairFeeConfig.pairConfig.leftSideFraction,
- orderPairFeeConfig.pairConfig.rightSideFraction,
+ pairConfig.leftSideFraction,
+ pairConfig.rightSideFraction,
submitOrderEvent.uuid,
submitOrderEvent.userLevel,
submitOrderEvent.direction,
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml b/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml
index a27cadb8f..52c433ffe 100644
--- a/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/config/AccountantKafkaConfig.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/config/AccountantKafkaConfig.kt
index 455ed860f..6ae6f37dd 100644
--- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/config/AccountantKafkaConfig.kt
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/config/AccountantKafkaConfig.kt
@@ -2,7 +2,9 @@ package co.nilin.opex.accountant.ports.kafka.listener.config
import co.nilin.opex.accountant.core.inout.KycLevelUpdatedEvent
import co.nilin.opex.accountant.ports.kafka.listener.consumer.*
+import co.nilin.opex.accountant.ports.kafka.listener.inout.DepositEvent
import co.nilin.opex.accountant.ports.kafka.listener.inout.FinancialActionResponseEvent
+import co.nilin.opex.accountant.ports.kafka.listener.inout.WithdrawRequestEvent
import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.TopicPartition
@@ -36,7 +38,7 @@ class AccountantKafkaConfig {
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*",
- JsonDeserializer.TYPE_MAPPINGS to "order_request_event:co.nilin.opex.accountant.ports.kafka.listener.inout.OrderRequestEvent,order_request_submit:co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequestEvent,order_request_cancel:co.nilin.opex.accountant.ports.kafka.listener.inout.OrderCancelRequestEvent,kyc_level_updated_event:co.nilin.opex.accountant.core.inout.KycLevelUpdatedEvent,fiAction_response_event:co.nilin.opex.accountant.ports.kafka.listener.inout.FinancialActionResponseEvent"
+ JsonDeserializer.TYPE_MAPPINGS to "order_request_event:co.nilin.opex.accountant.ports.kafka.listener.inout.OrderRequestEvent,order_request_submit:co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequestEvent,order_request_cancel:co.nilin.opex.accountant.ports.kafka.listener.inout.OrderCancelRequestEvent,kyc_level_updated_event:co.nilin.opex.accountant.core.inout.KycLevelUpdatedEvent,fiAction_response_event:co.nilin.opex.accountant.ports.kafka.listener.inout.FinancialActionResponseEvent,withdrawRequestEvent:co.nilin.opex.accountant.ports.kafka.listener.inout.WithdrawRequestEvent,depositEvent:co.nilin.opex.accountant.ports.kafka.listener.inout.DepositEvent"
)
}
@@ -55,6 +57,15 @@ class AccountantKafkaConfig {
return DefaultKafkaConsumerFactory(consumerConfigs)
}
+ @Bean("withdrawRequestConsumerFactory")
+ fun withdrawRequestConsumerFactory(@Qualifier("consumerConfig") consumerConfigs: Map): ConsumerFactory {
+ return DefaultKafkaConsumerFactory(consumerConfigs)
+ }
+ @Bean("depositConsumerFactory")
+ fun depositConsumerFactory(@Qualifier("consumerConfig") consumerConfigs: Map): ConsumerFactory {
+ return DefaultKafkaConsumerFactory(consumerConfigs)
+ }
+
@Autowired
@ConditionalOnBean(TradeKafkaListener::class)
fun configureTradeListener(
@@ -132,12 +143,32 @@ class AccountantKafkaConfig {
}
@Bean("kycLevelUpdatedProducerFactory")
- fun producerFactory(@Qualifier("consumerConfig") producerConfigs: Map): ProducerFactory {
+ fun kycLevelUpdatedProducerFactory(@Qualifier("consumerConfig") producerConfigs: Map): ProducerFactory {
return DefaultKafkaProducerFactory(producerConfigs)
}
@Bean("kycLevelUpdatedKafkaTemplate")
- fun kafkaTemplate(@Qualifier("kycLevelUpdatedProducerFactory") producerFactory: ProducerFactory): KafkaTemplate {
+ fun kycLevelUpdatedKafkaTemplate(@Qualifier("kycLevelUpdatedProducerFactory") producerFactory: ProducerFactory): KafkaTemplate {
+ return KafkaTemplate(producerFactory)
+ }
+
+ @Bean("withdrawRequestProducerFactory")
+ fun withdrawRequestProducerFactory(@Qualifier("consumerConfig") producerConfigs: Map): ProducerFactory {
+ return DefaultKafkaProducerFactory(producerConfigs)
+ }
+
+ @Bean("withdrawRequestKafkaTemplate")
+ fun withdrawRequestKafkaTemplate(@Qualifier("withdrawRequestProducerFactory") producerFactory: ProducerFactory): KafkaTemplate {
+ return KafkaTemplate(producerFactory)
+ }
+
+ @Bean("depositProducerFactory")
+ fun depositProducerFactory(@Qualifier("consumerConfig") producerConfigs: Map): ProducerFactory {
+ return DefaultKafkaProducerFactory(producerConfigs)
+ }
+
+ @Bean("depositKafkaTemplate")
+ fun depositKafkaTemplate(@Qualifier("depositProducerFactory") producerFactory: ProducerFactory): KafkaTemplate {
return KafkaTemplate(producerFactory)
}
@@ -156,6 +187,36 @@ class AccountantKafkaConfig {
container.start()
}
+ @Autowired
+ @ConditionalOnBean(WithdrawRequestKafkaListener::class)
+ fun configureWithdrawRequestEventListener(
+ listener: WithdrawRequestKafkaListener,
+ @Qualifier("withdrawRequestKafkaTemplate") template: KafkaTemplate,
+ @Qualifier("withdrawRequestConsumerFactory") consumerFactory: ConsumerFactory
+ ) {
+ val containerProps = ContainerProperties(Pattern.compile("withdraw_request"))
+ containerProps.messageListener = listener
+ val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps)
+ container.setBeanName("WithdrawRequestKafkaListenerContainer")
+ container.commonErrorHandler = createConsumerErrorHandler(template, "withdraw_request.DLT")
+ container.start()
+ }
+
+ @Autowired
+ @ConditionalOnBean(DepositKafkaListener::class)
+ fun configureDepositRequestEventListener(
+ listener: DepositKafkaListener,
+ @Qualifier("depositKafkaTemplate") template: KafkaTemplate,
+ @Qualifier("depositConsumerFactory") consumerFactory: ConsumerFactory
+ ) {
+ val containerProps = ContainerProperties(Pattern.compile("deposit"))
+ containerProps.messageListener = listener
+ val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps)
+ container.setBeanName("DepositKafkaListenerContainer")
+ container.commonErrorHandler = createConsumerErrorHandler(template, "deposit.DLT")
+ container.start()
+ }
+
private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler {
val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ ->
cr.headers().add("dlt-origin-module", "ACCOUNTANT".toByteArray())
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/DepositKafkaListener.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/DepositKafkaListener.kt
new file mode 100644
index 000000000..da4347aa9
--- /dev/null
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/DepositKafkaListener.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.accountant.ports.kafka.listener.consumer
+
+import co.nilin.opex.accountant.ports.kafka.listener.inout.DepositEvent
+import co.nilin.opex.accountant.ports.kafka.listener.spi.DepositListener
+import org.springframework.stereotype.Component
+
+@Component
+class DepositKafkaListener : EventConsumer()
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/WithdrawRequestKafkaListener.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/WithdrawRequestKafkaListener.kt
new file mode 100644
index 000000000..aaef0b48b
--- /dev/null
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/WithdrawRequestKafkaListener.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.accountant.ports.kafka.listener.consumer
+
+import co.nilin.opex.accountant.ports.kafka.listener.inout.WithdrawRequestEvent
+import co.nilin.opex.accountant.ports.kafka.listener.spi.WithdrawRequestListener
+import org.springframework.stereotype.Component
+
+@Component
+class WithdrawRequestKafkaListener : EventConsumer()
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/DepositEvent.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/DepositEvent.kt
new file mode 100644
index 000000000..37624a599
--- /dev/null
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/DepositEvent.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.accountant.ports.kafka.listener.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class DepositEvent(
+ val uuid: String,
+ val depositRef: String? = null,
+ val currency: String,
+ val amount: BigDecimal,
+ val createDate: LocalDateTime,
+)
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt
index 04876ee6f..f4d99e7b6 100644
--- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderRequestEvent.kt
@@ -2,4 +2,4 @@ package co.nilin.opex.accountant.ports.kafka.listener.inout
import co.nilin.opex.matching.engine.core.model.Pair
-abstract class OrderRequestEvent(val ouid:String, val uuid: String, val pair: Pair)
\ No newline at end of file
+abstract class OrderRequestEvent(val ouid: String, val uuid: String, val pair: Pair)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/WithdrawRequestEvent.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/WithdrawRequestEvent.kt
new file mode 100644
index 000000000..c76b45cb7
--- /dev/null
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/WithdrawRequestEvent.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.accountant.ports.kafka.listener.inout
+
+import co.nilin.opex.accountant.core.model.WithdrawStatus
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class WithdrawRequestEvent(
+ val uuid: String,
+ val withdrawId: Long? = null,
+ val currency: String,
+ val amount: BigDecimal,
+ val status: WithdrawStatus,
+ val createDate: LocalDateTime,
+)
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/spi/DepositListener.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/spi/DepositListener.kt
new file mode 100644
index 000000000..765899b4f
--- /dev/null
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/spi/DepositListener.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.accountant.ports.kafka.listener.spi
+
+import co.nilin.opex.accountant.ports.kafka.listener.inout.DepositEvent
+
+interface DepositListener : Listener
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/spi/WithdrawRequestListener.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/spi/WithdrawRequestListener.kt
new file mode 100644
index 000000000..98c63cd37
--- /dev/null
+++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/spi/WithdrawRequestListener.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.accountant.ports.kafka.listener.spi
+
+import co.nilin.opex.accountant.ports.kafka.listener.inout.WithdrawRequestEvent
+
+interface WithdrawRequestListener : Listener
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/pom.xml b/accountant/accountant-ports/accountant-persister-postgres/pom.xml
index 15d35337e..b90ec1cf5 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/pom.xml
+++ b/accountant/accountant-ports/accountant-persister-postgres/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt
new file mode 100644
index 000000000..7b9b743b0
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt
@@ -0,0 +1,30 @@
+package co.nilin.opex.accountant.ports.postgres.dao
+
+import co.nilin.opex.accountant.ports.postgres.model.CurrencyRateModel
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+
+@Repository
+interface CurrencyRateRepository : ReactiveCrudRepository {
+
+ @Query(
+ """
+ insert into currency_rate (base, quote, rate,update_date)
+ values (:base, :quote, :rate , now())
+ on conflict (base, quote)
+ do update set rate = excluded.rate , update_date = now()
+ """
+ )
+ fun createOrUpdate(base: String, quote: String, rate: BigDecimal): Mono
+
+ @Query("select rate from currency_rate where base = :base and quote = :quote")
+ fun findByBaseAndQuote(base: String, quote: String): Mono
+
+ @Query("select * from currency_rate where quote = :quote")
+ fun findAllByQuote(quote: String): Flux
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt
new file mode 100644
index 000000000..c1cefcb8b
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt
@@ -0,0 +1,45 @@
+package co.nilin.opex.accountant.ports.postgres.dao
+
+import co.nilin.opex.accountant.core.model.FeeConfig
+import co.nilin.opex.accountant.core.model.UserFee
+import co.nilin.opex.accountant.ports.postgres.model.FeeConfigModel
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+
+@Repository
+interface FeeConfigRepository : ReactiveCrudRepository {
+
+ @Query("select * from fee_config order by display_order")
+ fun findAllByOrder(): Flux
+
+ fun findByName(name: String): Mono
+
+ @Query(
+ """
+ SELECT name,display_order,maker_fee,taker_fee
+ FROM fee_config
+ WHERE
+ (
+ condition = 'AND'
+ AND (:assetVolume BETWEEN min_asset_volume AND COALESCE(max_asset_volume, :assetVolume))
+ AND (:tradeVolume BETWEEN min_trade_volume AND COALESCE(max_trade_volume, :tradeVolume))
+ )
+ OR
+ (
+ condition = 'OR'
+ AND (
+ (:assetVolume BETWEEN min_asset_volume AND COALESCE(max_asset_volume, :assetVolume))
+ OR (:tradeVolume BETWEEN min_trade_volume AND COALESCE(max_trade_volume, :tradeVolume))
+ )
+ )
+ ORDER BY display_order DESC
+ LIMIT 1
+ """
+ )
+ fun findMatchingConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): Mono
+
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt
index fa04c317d..3139f647d 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FinancialActionRepository.kt
@@ -41,10 +41,12 @@ interface FinancialActionRepository : ReactiveCrudRepository, status: FinancialActionStatus): Mono
- @Query("""
+ @Query(
+ """
select * from fi_actions fi
where status = 'CREATED'
and (parent_id is null or 'ERROR' != (select pfi.status from fi_actions pfi where pfi.id = fi.parent_id))
- """)
+ """
+ )
fun findReadyToProcess(of: Pageable): Flow
}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt
deleted file mode 100644
index 105bdc042..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.dao
-
-import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel
-import co.nilin.opex.matching.engine.core.model.OrderDirection
-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
-
-@Repository
-interface PairFeeConfigRepository : ReactiveCrudRepository {
-
- @Query("select * from pair_fee_config where pair_config_id = :pair and direction = :direction and user_level = :userLevel")
- fun findByPairAndDirectionAndUserLevel(
- @Param("pair") pair: String,
- @Param("direction") direction: OrderDirection,
- @Param("userLevel") userLevel: String
- ): Mono
-
- @Query("select * from pair_fee_config where direction = :direction and user_level = :userLevel")
- fun findByDirectionAndUserLevel(direction: OrderDirection, userLevel: String): Flux
-
-}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserDepositVolumeRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserDepositVolumeRepository.kt
new file mode 100644
index 000000000..56f5f4c03
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserDepositVolumeRepository.kt
@@ -0,0 +1,63 @@
+package co.nilin.opex.accountant.ports.postgres.dao
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.ports.postgres.model.UserDepositVolumeModel
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+import java.time.LocalDate
+
+@Repository
+interface UserDepositVolumeRepository : ReactiveCrudRepository {
+
+ @Query(
+ """
+ insert into user_deposit_volume (user_id, date, total_amount, quote_currency)
+ values (:userId, :date, :totalAmount, :quoteCurrency)
+ on conflict (user_id, date,quote_currency)
+ do update
+ set total_amount = user_deposit_volume.total_amount + EXCLUDED.total_amount
+ """
+ )
+ fun insertOrUpdate(
+ userId: String,
+ date: LocalDate,
+ totalAmount: BigDecimal,
+ quoteCurrency: String
+ ): Mono
+
+
+ @Query(
+ """
+ select sum(total_amount) as total_Amount
+ from user_deposit_volume
+ where user_id = :userId and date >= :startDate and quote_currency=:quoteCurrency
+ group by user_id
+ """
+ )
+ fun findTotalValueByUserAndAndDateAfter(
+ userId: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Mono
+
+
+ @Query(
+ """
+ select date, total_amount
+ from user_deposit_volume
+ where user_id = :userId
+ and date >= :startDate
+ and quote_currency = :quoteCurrency
+ order by date desc
+ """
+ )
+ fun findDailyDepositVolume(
+ userId: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Flux
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt
deleted file mode 100644
index 9156013b6..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.dao
-
-import co.nilin.opex.accountant.ports.postgres.model.UserLevelMapperModel
-import org.springframework.data.repository.reactive.ReactiveCrudRepository
-import org.springframework.stereotype.Repository
-import reactor.core.publisher.Mono
-
-@Repository
-interface UserLevelMapperRepository : ReactiveCrudRepository {
-
- fun findByUuid(uuid: String): Mono
-
-}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt
deleted file mode 100644
index 9bf546613..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.dao
-
-import co.nilin.opex.accountant.ports.postgres.model.UserLevelModel
-import org.springframework.data.r2dbc.repository.Query
-import org.springframework.data.repository.reactive.ReactiveCrudRepository
-import org.springframework.stereotype.Repository
-import reactor.core.publisher.Mono
-
-@Repository
-interface UserLevelRepository : ReactiveCrudRepository {
-
- fun findByLevel(level: String): Mono
-
- @Query("insert into user_level (level) values (:level) on conflict do nothing")
- fun insert(level: String): Mono
-
-}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt
new file mode 100644
index 000000000..d52675cb0
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt
@@ -0,0 +1,79 @@
+package co.nilin.opex.accountant.ports.postgres.dao
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.ports.postgres.model.UserTradeVolumeModel
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+import java.time.LocalDate
+
+@Repository
+interface UserTradeVolumeRepository : ReactiveCrudRepository {
+
+ @Query(
+ """
+ insert into user_trade_volume (user_id, currency, date, volume, total_amount, quote_currency)
+ values (:userId, :currency, :date, :volume, :totalAmount, :quoteCurrency)
+ on conflict (user_id, currency, date,quote_currency)
+ do update
+ set volume = user_trade_volume.volume + EXCLUDED.volume,
+ total_amount = user_trade_volume.total_amount + EXCLUDED.total_amount
+ """
+ )
+ fun insertOrUpdate(
+ userId: String,
+ currency: String,
+ date: LocalDate,
+ volume: BigDecimal,
+ totalAmount: BigDecimal,
+ quoteCurrency: String
+ ): Mono
+
+ @Query(
+ """
+ select sum(total_amount) as total_amount
+ from user_trade_volume
+ where user_id = :userId and date >= :startDate and quote_currency = :quoteCurrency
+ group by user_id
+ """
+ )
+ fun findTotalValueByUserAndAndDateAfter(
+ userId: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Mono
+
+ @Query(
+ """
+ select sum(total_amount) as total_amount
+ from user_trade_volume
+ where user_id = :userId and date >= :startDate and quote_currency = :quoteCurrency and currency = :currency
+ group by user_id
+ """
+ )
+ fun findTotalValueByUserAndAndDateAfterAndCurrency(
+ userId: String,
+ currency: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Mono
+
+ @Query(
+ """
+ select date, sum(total_amount) as total_amount
+ from user_trade_volume
+ where user_id = :userId
+ and date >= :startDate
+ and quote_currency = :quoteCurrency
+ group by date
+ """
+ )
+ fun findDailyTradeVolume(
+ userId: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Flux
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserWithdrawVolumeRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserWithdrawVolumeRepository.kt
new file mode 100644
index 000000000..8f65a4a38
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserWithdrawVolumeRepository.kt
@@ -0,0 +1,62 @@
+package co.nilin.opex.accountant.ports.postgres.dao
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.ports.postgres.model.UserWithdrawVolumeModel
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+import java.time.LocalDate
+
+@Repository
+interface UserWithdrawVolumeRepository : ReactiveCrudRepository {
+
+ @Query(
+ """
+ insert into user_withdraw_volume (user_id, date, total_amount, quote_currency)
+ values (:userId, :date, :totalAmount, :quoteCurrency)
+ on conflict (user_id, date,quote_currency)
+ do update
+ set total_amount = user_withdraw_volume.total_amount + EXCLUDED.total_amount
+ """
+ )
+ fun insertOrUpdate(
+ userId: String,
+ date: LocalDate,
+ totalAmount: BigDecimal,
+ quoteCurrency: String
+ ): Mono
+
+
+ @Query(
+ """
+ select sum(total_amount) as total_Amount
+ from user_withdraw_volume
+ where user_id = :userId and date >= :startDate and quote_currency=:quoteCurrency
+ group by user_id
+ """
+ )
+ fun findTotalValueByUserAndAndDateAfter(
+ userId: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Mono
+
+ @Query(
+ """
+ select date, total_amount
+ from user_withdraw_volume
+ where user_id = :userId
+ and date >= :startDate
+ and quote_currency = :quoteCurrency
+ order by date desc
+ """
+ )
+ fun findDailyWithdrawVolume(
+ userId: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): Flux
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/WithdrawLimitConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/WithdrawLimitConfigRepository.kt
new file mode 100644
index 000000000..f1e5b7806
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/WithdrawLimitConfigRepository.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.accountant.ports.postgres.dao
+
+import co.nilin.opex.accountant.ports.postgres.model.WithdrawLimitConfigModel
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Mono
+
+@Repository
+interface WithdrawLimitConfigRepository : ReactiveCrudRepository {
+
+ fun findByUserLevel(userLevel: String): Mono
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt
new file mode 100644
index 000000000..cbeb0dd06
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt
@@ -0,0 +1,19 @@
+package co.nilin.opex.accountant.ports.postgres.impl
+
+import co.nilin.opex.accountant.core.spi.CurrencyRatePersister
+import co.nilin.opex.accountant.ports.postgres.dao.CurrencyRateRepository
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+
+@Component
+class CurrencyRatePersisterImpl(private val repository: CurrencyRateRepository) : CurrencyRatePersister {
+
+ override suspend fun updateRate(base: String, quote: String, rate: BigDecimal) {
+ repository.createOrUpdate(base, quote, rate).awaitSingleOrNull()
+ }
+
+ override suspend fun getRate(base: String, quote: String): BigDecimal {
+ return repository.findByBaseAndQuote(base, quote).awaitSingleOrNull() ?: BigDecimal.ZERO
+ }
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt
new file mode 100644
index 000000000..13a8dae4d
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt
@@ -0,0 +1,65 @@
+package co.nilin.opex.accountant.ports.postgres.impl
+
+import co.nilin.opex.accountant.core.model.FeeConfig
+import co.nilin.opex.accountant.core.model.UserFee
+import co.nilin.opex.accountant.core.spi.FeeConfigService
+import co.nilin.opex.accountant.ports.postgres.dao.FeeConfigRepository
+import co.nilin.opex.accountant.ports.postgres.model.FeeConfigModel
+import co.nilin.opex.common.OpexError
+import kotlinx.coroutines.reactive.awaitFirst
+import kotlinx.coroutines.reactive.awaitFirstOrElse
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+
+@Component
+class FeeConfigServiceImpl(
+ private val feeConfigRepository: FeeConfigRepository,
+) : FeeConfigService {
+
+ override suspend fun loadFeeConfigs(): List {
+ return feeConfigRepository.findAllByOrder()
+ .collectList()
+ .awaitFirstOrElse { emptyList() }
+
+ }
+
+ override suspend fun saveFee(config: FeeConfig) {
+ feeConfigRepository.findByName(config.name).awaitFirstOrNull()?.let {
+ throw OpexError.FeeConfigAlreadyExists.exception("FeeConfig with name '${config.name}' already exists")
+ }
+ feeConfigRepository.save(config.asFeeConfigModel()).awaitFirst()
+ }
+
+ override suspend fun updateFee(config: FeeConfig) {
+ feeConfigRepository.findByName(config.name).awaitFirstOrNull()
+ ?: throw OpexError.FeeConfigNotFound.exception("FeeConfig with name '${config.name}' not found")
+
+ feeConfigRepository.save(config.asFeeConfigModel()).awaitFirst()
+ }
+
+ override suspend fun loadFeeConfig(name: String): FeeConfig {
+ return feeConfigRepository.findByName(name).awaitFirstOrNull()
+ ?: throw OpexError.FeeConfigNotFound.exception("FeeConfig with name '${name}' not found")
+ }
+
+ override suspend fun loadMatchingFeeConfig(
+ assetVolume: BigDecimal,
+ tradeVolume: BigDecimal
+ ): UserFee {
+ return feeConfigRepository.findMatchingConfig(assetVolume, tradeVolume).awaitFirst()
+ }
+
+ private fun FeeConfig.asFeeConfigModel() =
+ FeeConfigModel(
+ name,
+ displayOrder,
+ minAssetVolume,
+ maxAssetVolume,
+ minTradeVolume,
+ maxTradeVolume,
+ makerFee,
+ takerFee,
+ condition
+ )
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt
index b347977cc..86d9dcd89 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt
@@ -3,7 +3,6 @@ package co.nilin.opex.accountant.ports.postgres.impl
import co.nilin.opex.accountant.core.model.FinancialAction
import co.nilin.opex.accountant.core.model.FinancialActionStatus
import co.nilin.opex.accountant.core.spi.FinancialActionPersister
-import co.nilin.opex.accountant.core.spi.JsonMapper
import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionErrorRepository
import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository
import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRetryRepository
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt
index 78487926e..5fe137ca4 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt
@@ -1,24 +1,17 @@
package co.nilin.opex.accountant.ports.postgres.impl
import co.nilin.opex.accountant.core.model.PairConfig
-import co.nilin.opex.accountant.core.model.PairFeeConfig
import co.nilin.opex.accountant.core.spi.PairConfigLoader
import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository
-import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository
import co.nilin.opex.accountant.ports.postgres.model.PairConfigModel
-import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel
import co.nilin.opex.common.OpexError
import co.nilin.opex.matching.engine.core.model.OrderDirection
import kotlinx.coroutines.reactive.awaitFirstOrElse
-import kotlinx.coroutines.reactive.awaitFirstOrNull
-import kotlinx.coroutines.reactive.awaitSingle
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.stereotype.Component
@Component
class PairConfigLoaderImpl(
private val pairConfigRepository: PairConfigRepository,
- private val pairFeeConfigRepository: PairFeeConfigRepository
) : PairConfigLoader {
override suspend fun loadPairConfigs(): List {
@@ -28,89 +21,6 @@ class PairConfigLoaderImpl(
.map { it.asPairConfig() }
}
- override suspend fun loadPairFeeConfigs(): List {
- return pairFeeConfigRepository.findAll()
- .collectList()
- .awaitFirstOrElse { emptyList() }
- .map {
- val pairConfig = pairConfigRepository.findById(it.pairConfigId).awaitSingle().asPairConfig()
- PairFeeConfig(pairConfig, it.direction, it.userLevel, it.makerFee, it.takerFee)
- }
- }
-
- override suspend fun loadPairFeeConfigs(direction: OrderDirection, userLevel: String): List {
- return pairFeeConfigRepository.findByDirectionAndUserLevel(direction, userLevel)
- .collectList()
- .awaitFirstOrElse { emptyList() }
- .map {
- val pairConfig = pairConfigRepository.findById(it.pairConfigId).awaitSingle().asPairConfig()
- PairFeeConfig(
- pairConfig,
- it.direction,
- it.userLevel,
- it.makerFee,
- it.takerFee
- )
- }
- }
-
- override suspend fun loadPairFeeConfigs(
- pair: String,
- direction: OrderDirection,
- userLevel: String
- ): PairFeeConfig? {
- val fee = pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(pair, direction, userLevel)
- .awaitSingleOrNull() ?: return null
- val pairConfig = pairConfigRepository.findById(fee.pairConfigId).awaitSingle().asPairConfig()
- return PairFeeConfig(
- pairConfig,
- fee.direction,
- fee.userLevel,
- fee.makerFee,
- fee.takerFee
- )
- }
-
- override suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig {
- val pairConfig = pairConfigRepository.findById(pair).awaitFirstOrElse {
- throw OpexError.InvalidPair.messageFormattedException(pair)
- }
-
- var pairFeeConfig: PairFeeConfigModel?
- if (userLevel.isEmpty()) {
- pairFeeConfig = pairFeeConfigRepository
- .findByPairAndDirectionAndUserLevel(pair, direction, "*")
- .awaitFirstOrElse {
- throw OpexError.InvalidPair.messageFormattedException(pair)
- }
- } else {
- pairFeeConfig = pairFeeConfigRepository
- .findByPairAndDirectionAndUserLevel(pair, direction, userLevel)
- .awaitFirstOrNull()
- if (pairFeeConfig == null) {
- pairFeeConfig = pairFeeConfigRepository
- .findByPairAndDirectionAndUserLevel(pair, direction, "*")
- .awaitFirstOrElse {
- throw OpexError.InvalidPair.messageFormattedException(pair)
- }
- }
- }
-
- return PairFeeConfig(
- PairConfig(
- pair,
- pairConfig.leftSideWalletSymbol,
- pairConfig.rightSideWalletSymbol,
- pairConfig.leftSideFraction,
- pairConfig.rightSideFraction
- ),
- pairFeeConfig!!.direction,
- pairFeeConfig.userLevel,
- pairFeeConfig.makerFee,
- pairFeeConfig.takerFee
- )
- }
-
override suspend fun load(pair: String, direction: OrderDirection): PairConfig {
return pairConfigRepository
.findById(pair).awaitFirstOrElse {
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt
index 8bfa95402..2aed7a703 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/TempEventPersisterImpl.kt
@@ -6,7 +6,6 @@ import co.nilin.opex.accountant.ports.postgres.dao.TempEventRepository
import co.nilin.opex.accountant.ports.postgres.model.TempEventModel
import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent
import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactive.awaitFirstOrNull
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserDepositVolumePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserDepositVolumePersisterImpl.kt
new file mode 100644
index 000000000..580c3ca36
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserDepositVolumePersisterImpl.kt
@@ -0,0 +1,82 @@
+package co.nilin.opex.accountant.ports.postgres.impl
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.model.WithdrawStatus
+import co.nilin.opex.accountant.core.spi.CurrencyRatePersister
+import co.nilin.opex.accountant.core.spi.UserDepositVolumePersister
+import co.nilin.opex.accountant.ports.postgres.dao.UserDepositVolumeRepository
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.math.RoundingMode
+import java.time.LocalDate
+
+
+@Component
+class UserDepositVolumePersisterImpl(
+ private val repository: UserDepositVolumeRepository,
+ private val currencyRatePersister: CurrencyRatePersister,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+ @Value("\${app.deposit-volume-calculation-currency}") private val calculationCurrency: String,
+ @Value("\${app.deposit-volume-calculation-currency-precision:2}") private val calculationCurrencyPrecision: Int
+
+) : UserDepositVolumePersister {
+
+ override suspend fun update(
+ userId: String,
+ currency: String,
+ amount: BigDecimal,
+ date: LocalDateTime
+ ) {
+ val rate = if (currency == calculationCurrency) BigDecimal.ONE
+ else currencyRatePersister.getRate(currency, calculationCurrency)
+
+ val signedAmount = amount.multiply(rate).setScale(calculationCurrencyPrecision, RoundingMode.DOWN)
+
+ repository.insertOrUpdate(
+ userId,
+ date.atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate(),
+ signedAmount,
+ calculationCurrency
+ ).awaitSingleOrNull()
+ }
+
+ override suspend fun getTotalValueByUserAndDateAfter(
+ uuid: String,
+ startDate: LocalDateTime
+ ): BigDecimal {
+ return repository.findTotalValueByUserAndAndDateAfter(
+ uuid,
+ startDate.atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate(),
+ calculationCurrency
+ ).awaitFirstOrNull() ?: BigDecimal.ZERO
+ }
+
+ override suspend fun getLastDaysDeposit(
+ userId: String,
+ startDate: LocalDate?,
+ quatCurrency: String?,
+ lastDays: Long
+ ): List {
+
+ val startDate = startDate ?: LocalDate
+ .now(ZoneOffset.of(zoneOffsetString))
+ .minusDays(lastDays)
+
+ return repository
+ .findDailyDepositVolume(userId, startDate, quatCurrency?:calculationCurrency)
+ .map {
+ DailyAmount(
+ date = it.date,
+ totalAmount = it.totalAmount
+ )
+ }
+ .collectList()
+ .awaitSingle()
+ }
+}
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt
deleted file mode 100644
index ac2817040..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.impl
-
-import co.nilin.opex.accountant.core.model.KycLevel
-import co.nilin.opex.accountant.core.spi.UserLevelLoader
-import co.nilin.opex.accountant.ports.postgres.dao.UserLevelMapperRepository
-import co.nilin.opex.accountant.ports.postgres.dao.UserLevelRepository
-import co.nilin.opex.accountant.ports.postgres.model.UserLevelMapperModel
-import kotlinx.coroutines.reactor.awaitSingleOrNull
-import org.springframework.stereotype.Component
-
-@Component
-class UserLevelLoaderImpl(private val userLevelMapperRepository: UserLevelMapperRepository,
- private val userLevelRepository: UserLevelRepository) : UserLevelLoader {
-
- override suspend fun load(uuid: String): String {
- val mapper = userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull()
- return mapper?.userLevel ?: "*"
- }
-
- override suspend fun update(uuid: String, userLevel: KycLevel) {
-
- userLevelRepository.findByLevel(userLevel.name).awaitSingleOrNull()?.let {
- userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull()
- ?.let { userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name)).awaitSingleOrNull() }
- ?: run { userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull() }
- }?:
- run {
- userLevelRepository.insert(userLevel.name) .awaitSingleOrNull()
- userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull()
- ?.let { userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name)).awaitSingleOrNull() }
- ?: run { userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull() }
-
- }
- }
-}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserTradeVolumePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserTradeVolumePersisterImpl.kt
new file mode 100644
index 000000000..9291c0d36
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserTradeVolumePersisterImpl.kt
@@ -0,0 +1,73 @@
+package co.nilin.opex.accountant.ports.postgres.impl
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.spi.UserTradeVolumePersister
+import co.nilin.opex.accountant.ports.postgres.dao.UserTradeVolumeRepository
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.ZoneOffset
+
+@Component
+class UserTradeVolumePersisterImpl(
+ private val repository: UserTradeVolumeRepository,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+ @Value("\${app.trade-volume-calculation-currency}") private val calculationCurrency: String,
+
+ ) : UserTradeVolumePersister {
+
+ override suspend fun update(
+ userId: String,
+ currency: String,
+ date: LocalDate,
+ volume: BigDecimal,
+ totalAmount: BigDecimal,
+ quoteCurrency: String
+ ) {
+ repository.insertOrUpdate(userId, currency, date, volume, totalAmount, quoteCurrency).awaitSingleOrNull()
+ }
+
+ override suspend fun getUserTotalTradeVolume(
+ uuid: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): BigDecimal? {
+ return repository.findTotalValueByUserAndAndDateAfter(uuid, startDate, quoteCurrency).awaitSingleOrNull()
+ }
+
+ override suspend fun getUserTotalTradeVolumeByCurrency(
+ uuid: String,
+ currency: String,
+ startDate: LocalDate,
+ quoteCurrency: String
+ ): BigDecimal? {
+ return repository.findTotalValueByUserAndAndDateAfterAndCurrency(uuid, currency, startDate, quoteCurrency)
+ .awaitSingleOrNull()
+ }
+
+ override suspend fun getLastDaysTrade(
+ userId: String,
+ startDate: LocalDate?,
+ quatCurrency: String?,
+ lastDays: Long
+ ): List {
+
+ val startDate = startDate ?: LocalDate
+ .now(ZoneOffset.of(zoneOffsetString))
+ .minusDays(lastDays)
+
+ return repository
+ .findDailyTradeVolume(userId, startDate, quatCurrency ?: calculationCurrency)
+ .map {
+ DailyAmount(
+ date = it.date,
+ totalAmount = it.totalAmount
+ )
+ }
+ .collectList()
+ .awaitSingle()
+ }
+}
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserWithdrawVolumePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserWithdrawVolumePersisterImpl.kt
new file mode 100644
index 000000000..6e72f5bcc
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserWithdrawVolumePersisterImpl.kt
@@ -0,0 +1,84 @@
+package co.nilin.opex.accountant.ports.postgres.impl
+
+import co.nilin.opex.accountant.core.model.DailyAmount
+import co.nilin.opex.accountant.core.model.WithdrawStatus
+import co.nilin.opex.accountant.core.spi.CurrencyRatePersister
+import co.nilin.opex.accountant.core.spi.UserWithdrawVolumePersister
+import co.nilin.opex.accountant.ports.postgres.dao.UserWithdrawVolumeRepository
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+
+@Component
+class UserWithdrawVolumePersisterImpl(
+ private val repository: UserWithdrawVolumeRepository,
+ private val currencyRatePersister: CurrencyRatePersister,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+ @Value("\${app.withdraw-volume-calculation-currency}") private val calculationCurrency: String,
+ @Value("\${app.withdraw-volume-calculation-currency-precision:2}") private val calculationCurrencyPrecision: Int
+
+) : UserWithdrawVolumePersister {
+
+ override suspend fun update(
+ userId: String,
+ currency: String,
+ amount: BigDecimal,
+ date: LocalDateTime,
+ withdrawStatus: WithdrawStatus
+ ) {
+ val rate = if (currency == calculationCurrency) BigDecimal.ONE
+ else currencyRatePersister.getRate(currency, calculationCurrency)
+
+ val signedAmount = amount.multiply(rate).setScale(calculationCurrencyPrecision, RoundingMode.DOWN)
+ .let { if (withdrawStatus == WithdrawStatus.CANCELED || withdrawStatus == WithdrawStatus.REJECTED) it.negate() else it }
+
+ repository.insertOrUpdate(
+ userId,
+ date.atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate(),
+ signedAmount,
+ calculationCurrency
+ ).awaitSingleOrNull()
+ }
+
+ override suspend fun getTotalValueByUserAndDateAfter(
+ uuid: String,
+ startDate: LocalDateTime
+ ): BigDecimal {
+ return repository.findTotalValueByUserAndAndDateAfter(
+ uuid,
+ startDate.atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate(),
+ calculationCurrency
+ ).awaitFirstOrNull() ?: BigDecimal.ZERO
+ }
+
+ override suspend fun getLastDaysWithdraw(
+ userId: String,
+ startDate: LocalDate?,
+ quatCurrency: String?,
+ lastDays: Long
+ ): List {
+
+ val startDate = startDate ?: LocalDate
+ .now(ZoneOffset.of(zoneOffsetString))
+ .minusDays(lastDays)
+
+ return repository
+ .findDailyWithdrawVolume(userId, startDate, quatCurrency?:calculationCurrency)
+ .map {
+ DailyAmount(
+ date = it.date,
+ totalAmount = it.totalAmount
+ )
+ }
+ .collectList()
+ .awaitSingle()
+ }
+}
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/WithdrawLimitManagerImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/WithdrawLimitManagerImpl.kt
new file mode 100644
index 000000000..62f69f640
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/WithdrawLimitManagerImpl.kt
@@ -0,0 +1,55 @@
+package co.nilin.opex.accountant.ports.postgres.impl
+
+import co.nilin.opex.accountant.core.model.WithdrawLimitConfig
+import co.nilin.opex.accountant.core.spi.CurrencyRatePersister
+import co.nilin.opex.accountant.core.spi.WithdrawLimitManager
+import co.nilin.opex.accountant.ports.postgres.dao.UserWithdrawVolumeRepository
+import co.nilin.opex.accountant.ports.postgres.dao.WithdrawLimitConfigRepository
+import co.nilin.opex.common.OpexError
+import kotlinx.coroutines.reactive.awaitFirst
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+@Component
+class WithdrawLimitManagerImpl(
+ private val userWithdrawVolumeRepository: UserWithdrawVolumeRepository,
+ private val withdrawLimitConfigRepository: WithdrawLimitConfigRepository,
+ private val currencyRatePersister: CurrencyRatePersister,
+ @Value("\${app.zone-offset}") private val zoneOffsetString: String,
+ @Value("\${app.withdraw-volume-calculation-currency}") private val calculationCurrency: String
+
+) : WithdrawLimitManager {
+
+ override suspend fun canWithdraw(uuid: String, userLevel: String, currency: String, amount: BigDecimal): Boolean {
+
+ val rate = if (currency == calculationCurrency) BigDecimal.ONE
+ else currencyRatePersister.getRate(currency, calculationCurrency)
+
+ val withdrawLimitConfig = (withdrawLimitConfigRepository.findByUserLevel(userLevel).awaitFirstOrNull()
+ ?: throw OpexError.WithdrawLimitConfigNotFound.exception())
+
+ val userWithdrawVolume = userWithdrawVolumeRepository.findTotalValueByUserAndAndDateAfter(
+ uuid,
+ LocalDateTime.now().atOffset(ZoneOffset.of(zoneOffsetString)).toLocalDate(),
+ calculationCurrency
+ ).awaitFirstOrNull()
+
+ return ((userWithdrawVolume ?: BigDecimal.ZERO) + (amount.multiply(rate))) <= withdrawLimitConfig.dailyMaxAmount
+ }
+
+ override suspend fun getAll(): List {
+ return withdrawLimitConfigRepository.findAll()
+ .map { model ->
+ WithdrawLimitConfig(
+ name = model.name,
+ dailyMaxAmount = model.dailyMaxAmount
+ )
+ }
+ .collectList()
+ .awaitFirst()
+ }
+}
\ No newline at end of file
diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CurrencyRateModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/CurrencyRateModel.kt
similarity index 59%
rename from market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CurrencyRateModel.kt
rename to accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/CurrencyRateModel.kt
index 490f622da..ab21c2c48 100644
--- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/model/CurrencyRateModel.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/CurrencyRateModel.kt
@@ -1,15 +1,15 @@
-package co.nilin.opex.market.ports.postgres.model
+package co.nilin.opex.accountant.ports.postgres.model
-import co.nilin.opex.market.core.inout.RateSource
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
+import java.time.LocalDateTime
@Table("currency_rate")
data class CurrencyRateModel(
- @Id val id: Long? = null,
val base: String,
val quote: String,
- val source: RateSource,
- val rate: BigDecimal
+ val rate: BigDecimal,
+ @Id val id: Long? = null,
+ val updateDate : LocalDateTime = LocalDateTime.now()
)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt
new file mode 100644
index 000000000..49446a51c
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt
@@ -0,0 +1,21 @@
+package co.nilin.opex.accountant.ports.postgres.model
+
+import co.nilin.opex.accountant.core.model.Condition
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+
+@Table(name = "fee_config")
+data class FeeConfigModel(
+ @Id
+ val name: String,
+ val displayOrder: Int,
+ val minAssetVolume: BigDecimal,
+ val maxAssetVolume: BigDecimal? = null,
+ val minTradeVolume: BigDecimal,
+ val maxTradeVolume: BigDecimal? = null,
+ val makerFee: BigDecimal,
+ val takerFee: BigDecimal,
+ val condition: Condition,
+)
+
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt
index 59d10c675..a8b79712a 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt
@@ -4,7 +4,6 @@ import co.nilin.opex.accountant.core.model.FinancialActionCategory
import co.nilin.opex.accountant.core.model.FinancialActionStatus
import co.nilin.opex.accountant.core.model.WalletType
import org.springframework.data.annotation.Id
-import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
import java.time.LocalDateTime
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt
deleted file mode 100644
index cd46a2abc..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.model
-
-import org.springframework.data.relational.core.mapping.Column
-import org.springframework.data.relational.core.mapping.Table
-import java.math.BigDecimal
-
-@Table("pair_fee_config")
-data class PairFeeConfigModel(
- val id: Long?,
- @Column("pair_config_id") val pairConfigId: String,
- @Column("direction") val direction: String,
- @Column("user_level") val userLevel: String,
- @Column("maker_fee") val makerFee: BigDecimal,
- @Column("taker_fee") val takerFee: BigDecimal
-)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserDepositVolumeModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserDepositVolumeModel.kt
new file mode 100644
index 000000000..5f0cbead1
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserDepositVolumeModel.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.accountant.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Column
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+import java.time.LocalDate
+
+@Table("user_deposit_volume")
+data class UserDepositVolumeModel(
+ @Id val id: Long? = null,
+ val userId: String,
+ val date: LocalDate,
+ val totalAmount: BigDecimal,
+ val quoteCurrency: String,
+)
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt
deleted file mode 100644
index f2f2f08f3..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.model
-
-import org.springframework.data.annotation.Id
-import org.springframework.data.relational.core.mapping.Table
-
-@Table("user_level_mapper")
-data class UserLevelMapperModel(
- @Id val id: Long?,
- val uuid: String,
- val userLevel: String
-)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt
deleted file mode 100644
index 2e9af91b0..000000000
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package co.nilin.opex.accountant.ports.postgres.model
-
-import org.springframework.data.annotation.Id
-import org.springframework.data.relational.core.mapping.Table
-
-@Table("user_level")
-data class UserLevelModel(@Id val level: String)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt
new file mode 100644
index 000000000..f7b4b6f7e
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.accountant.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Column
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+import java.time.LocalDate
+
+@Table("user_trade_volume")
+class UserTradeVolumeModel(
+ val userId: String,
+ val currency: String,
+ val date: LocalDate,
+ val volume: BigDecimal,
+ val totalAmount: BigDecimal,
+ val quoteCurrency: String,
+ @Id val id: Long? = null
+)
\ No newline at end of file
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserWithdrawVolumeModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserWithdrawVolumeModel.kt
new file mode 100644
index 000000000..caa4ec072
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserWithdrawVolumeModel.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.accountant.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Column
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+import java.time.LocalDate
+
+@Table("user_withdraw_volume")
+data class UserWithdrawVolumeModel(
+ @Id val id: Long? = null,
+ val userId: String,
+ val date: LocalDate,
+ val totalAmount: BigDecimal,
+ val quoteCurrency: String,
+)
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/WithdrawLimitConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/WithdrawLimitConfigModel.kt
new file mode 100644
index 000000000..c3b283a01
--- /dev/null
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/WithdrawLimitConfigModel.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.accountant.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+import java.math.BigDecimal
+
+@Table("withdraw_limit_config")
+data class WithdrawLimitConfigModel(
+ @Id val name: String,
+ val userLevel: String,
+ val dailyMaxAmount: BigDecimal,
+
+ )
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql
index beeddfa88..c4d6999c6 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql
@@ -45,10 +45,10 @@ CREATE TABLE IF NOT EXISTS fi_actions
create_date TIMESTAMP NOT NULL,
status VARCHAR(20)
);
-CREATE INDEX IF NOT EXISTS idx_fi_actions_symbol ON fi_actions(symbol);
-CREATE INDEX IF NOT EXISTS idx_fi_event_type ON fi_actions(event_type);
-CREATE INDEX IF NOT EXISTS idx_fi_actions_status ON fi_actions(status);
-CREATE INDEX IF NOT EXISTS idx_fi_actions_pointer ON fi_actions(pointer);
+CREATE INDEX IF NOT EXISTS idx_fi_actions_symbol ON fi_actions (symbol);
+CREATE INDEX IF NOT EXISTS idx_fi_event_type ON fi_actions (event_type);
+CREATE INDEX IF NOT EXISTS idx_fi_actions_status ON fi_actions (status);
+CREATE INDEX IF NOT EXISTS idx_fi_actions_pointer ON fi_actions (pointer);
ALTER TABLE fi_actions
ADD COLUMN IF NOT EXISTS category_name VARCHAR(36);
@@ -84,36 +84,79 @@ CREATE TABLE IF NOT EXISTS pair_config
UNIQUE (left_side_wallet_symbol, right_side_wallet_symbol)
);
-CREATE TABLE IF NOT EXISTS user_level
+CREATE TABLE IF NOT EXISTS temp_events
+(
+ id SERIAL PRIMARY KEY,
+ ouid VARCHAR(72) NOT NULL,
+ event_type VARCHAR(72) NOT NULL,
+ event_body TEXT NOT NULL,
+ event_date TIMESTAMP NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS user_trade_volume
(
- level VARCHAR(36) PRIMARY KEY
+ id SERIAL PRIMARY KEY,
+ user_id VARCHAR(36) NOT NULL,
+ currency TEXT NOT NULL,
+ date DATE not null,
+ volume decimal not null,
+ total_amount decimal not null,
+ quote_currency VARCHAR(50) NOT NULL,
+ unique (user_id, currency, date , quote_currency)
);
-CREATE TABLE IF NOT EXISTS pair_fee_config
+CREATE TABLE IF NOT EXISTS currency_rate
(
- id SERIAL PRIMARY KEY,
- pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair),
- direction VARCHAR(36) NOT NULL,
- user_level VARCHAR(36) NOT NULL REFERENCES user_level (level),
- maker_fee DECIMAL NOT NULL,
- taker_fee DECIMAL NOT NULL,
- UNIQUE (direction, user_level, pair_config_id)
+ id SERIAL PRIMARY KEY,
+ base VARCHAR(25) NOT NULL,
+ quote VARCHAR(25) NOT NULL,
+ rate DECIMAL NOT NULL DEFAULT 0,
+ update_date TIMESTAMP NOT NULL,
+ UNIQUE (base, quote)
);
-CREATE TABLE IF NOT EXISTS user_level_mapper
+ CREATE TABLE IF NOT EXISTS fee_config (
+ name VARCHAR(50) PRIMARY KEY,
+ display_order INTEGER NOT NULL UNIQUE,
+ min_asset_volume Decimal NOT NULL,
+ max_asset_volume Decimal,
+ min_trade_volume Decimal NOT NULL,
+ max_trade_volume Decimal,
+ maker_fee Decimal NOT NULL,
+ taker_fee Decimal NOT NULL,
+ condition VARCHAR(10) NOT NULL
+ );
+
+ DROP TABLE IF EXISTS pair_fee_config;
+ DROP TABLE IF EXISTS user_fee;
+ DROP TABLE IF EXISTS user_level_mapper;
+ DROP TABLE IF EXISTS user_level;
+
+CREATE TABLE IF NOT EXISTS user_withdraw_volume
(
id SERIAL PRIMARY KEY,
- uuid VARCHAR(36) NOT NULL UNIQUE,
- user_level VARCHAR(36) NOT NULL REFERENCES user_level (level)
+ user_id VARCHAR(36) NOT NULL,
+ date DATE not null,
+ total_amount decimal not null,
+ quote_currency VARCHAR(50) NOT NULL,
+ unique (user_id, date,quote_currency)
);
-CREATE TABLE IF NOT EXISTS temp_events
+CREATE TABLE IF NOT EXISTS withdraw_limit_config
+(
+ name VARCHAR(50) PRIMARY KEY,
+ user_level VARCHAR(20) NOT NULL UNIQUE,
+ daily_max_amount decimal not null
+);
+
+CREATE TABLE IF NOT EXISTS user_deposit_volume
(
id SERIAL PRIMARY KEY,
- ouid VARCHAR(72) NOT NULL,
- event_type VARCHAR(72) NOT NULL,
- event_body TEXT NOT NULL,
- event_date TIMESTAMP NOT NULL
+ user_id VARCHAR(36) NOT NULL,
+ date DATE not null,
+ total_amount decimal not null,
+ quote_currency VARCHAR(50) NOT NULL,
+ unique (user_id, date,quote_currency)
);
COMMIT;
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt
index 73da00a7f..37a22ed39 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt
@@ -1,13 +1,11 @@
package co.nilin.opex.accountant.ports.postgres
import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository
-import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository
import co.nilin.opex.accountant.ports.postgres.impl.PairConfigLoaderImpl
import co.nilin.opex.matching.engine.core.model.OrderDirection
import co.nilin.opex.utility.error.data.OpexException
import io.mockk.every
import io.mockk.mockk
-import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
@@ -22,11 +20,7 @@ class PairConfigLoaderTest {
every { findAll() } returns Flux.just(Valid.pairConfigModel, Valid.pairConfigModel)
every { findById(any() as String) } returns Mono.just(Valid.pairConfigModel)
}
- private val pairFeeConfigRepository = mockk {
- every { findAll() } returns Flux.just(Valid.pairFeeConfigModel, Valid.pairFeeConfigModel)
- every { findByPairAndDirectionAndUserLevel(any(), any(), any()) } returns Mono.just(Valid.pairFeeConfigModel)
- }
- private val pairConfigLoader = PairConfigLoaderImpl(pairConfigRepository, pairFeeConfigRepository)
+ private val pairConfigLoader = PairConfigLoaderImpl(pairConfigRepository)
@Test
fun givenPairConfigs_whenListNotEmpty_resultIsNotEmptyAndValid(): Unit = runBlocking {
@@ -41,84 +35,14 @@ class PairConfigLoaderTest {
}
}
- @Test
- fun givenPairFeeConfigs_whenListNotEmpty_resultIsNotEmptyAndValid(): Unit = runBlocking {
- val configs = pairConfigLoader.loadPairFeeConfigs()
- assertThat(configs.size).isEqualTo(2)
- with(configs[1]) {
- assertThat(pairConfig.pair).isEqualTo(Valid.pairConfig.pair)
- assertThat(userLevel).isEqualTo(Valid.pairFeeConfigModel.userLevel)
- assertThat(direction).isEqualTo(Valid.pairFeeConfigModel.direction)
- assertThat(makerFee).isEqualTo(Valid.pairFeeConfigModel.makerFee)
- assertThat(takerFee).isEqualTo(Valid.pairFeeConfigModel.takerFee)
- }
- }
-
@Test
fun givenPairDirectionUserLevel_whenPairConfigNotFound_throwsException(): Unit = runBlocking {
every { pairConfigRepository.findById(any() as String) } returns Mono.empty()
assertThatThrownBy {
- runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "*") }
- }.isInstanceOf(OpexException::class.java)
- }
-
- @Test
- fun givenPairDirection_whenUserLevelEmpty_loadWithDefaultUserLevel(): Unit = runBlocking {
- val pair = pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "")
- assertThat(pair).isNotNull
- verify(exactly = 1) {
- pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(
- eq("BTC_USDT"),
- eq(OrderDirection.BID),
- eq("*")
- )
- }
- }
-
- @Test
- fun givenPairDirection_whenPairFeeConfigNotFound_throwsException(): Unit = runBlocking {
- every {
- pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("*"))
- } returns Mono.empty()
-
- assertThatThrownBy {
- runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "") }
+ runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID) }
}.isInstanceOf(OpexException::class.java)
}
- @Test
- fun givenPairDirectionUserLevel_whenPairFeeConfigNotFound_loadWithDefaultUserLevel(): Unit = runBlocking {
- every {
- pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("1"))
- } returns Mono.empty()
-
- val pair = pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "1")
- assertThat(pair).isNotNull
- verify(exactly = 1) {
- pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(
- eq("BTC_USDT"),
- eq(OrderDirection.BID),
- eq("*")
- )
- }
- }
-
- @Test
- fun givenPairDirectionUserLevel_whenPairFeeConfigNotFoundWithActualAndDefaultUserLevel_throwsException(): Unit =
- runBlocking {
- every {
- pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("1"))
- } returns Mono.empty()
-
- every {
- pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("*"))
- } returns Mono.empty()
-
- assertThatThrownBy {
- runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "1") }
- }.isInstanceOf(OpexException::class.java)
- }
-
@Test
fun givenPairDirection_whenPairConfigNotFound_throwException(): Unit = runBlocking {
every { pairConfigRepository.findById(any() as String) } returns Mono.empty()
diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt
index af2eb5f88..b96047449 100644
--- a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt
+++ b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt
@@ -85,15 +85,6 @@ object Valid {
0.01.toBigDecimal()
)
- val pairFeeConfigModel = PairFeeConfigModel(
- 1,
- "BTC_USDT",
- "BID",
- "1",
- 0.01.toBigDecimal(),
- 0.01.toBigDecimal()
- )
-
class TestCoreEvent(val leftSidePair: String, val rightSidePair: String) :
CoreEvent(Pair(leftSidePair, rightSidePair), currentTime)
diff --git a/accountant/accountant-ports/accountant-submitter-kafka/pom.xml b/accountant/accountant-ports/accountant-submitter-kafka/pom.xml
index 2759d98c3..353a37eba 100644
--- a/accountant/accountant-ports/accountant-submitter-kafka/pom.xml
+++ b/accountant/accountant-ports/accountant-submitter-kafka/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml
index 3819743c8..17fecd3ba 100644
--- a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml
+++ b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt
index b559aa504..2ba1c3c02 100644
--- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt
+++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/config/WebClientConfig.kt
@@ -5,7 +5,6 @@ import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalanc
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler
diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt
index c9ed17d3e..ff7078afd 100644
--- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt
+++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt
@@ -1,10 +1,13 @@
package co.nilin.opex.accountant.ports.walletproxy.proxy
+import co.nilin.opex.accountant.core.model.CurrencyPrice
+import co.nilin.opex.accountant.core.model.TotalAssetsSnapshot
import co.nilin.opex.accountant.core.model.WalletType
import co.nilin.opex.accountant.core.spi.WalletProxy
import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse
import co.nilin.opex.accountant.ports.walletproxy.data.TransferResult
import kotlinx.coroutines.reactive.awaitFirst
+import kotlinx.coroutines.reactive.awaitFirstOrNull
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
@@ -56,4 +59,26 @@ class WalletProxyImpl(
.awaitFirst()
.result
}
+
+ override suspend fun getUserTotalAssets(
+ uuid: String,
+ ): TotalAssetsSnapshot? {
+ return webClient.get()
+ .uri("$walletBaseUrl/stats/total-assets/$uuid")
+ .header("Content-Type", "application/json")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitFirstOrNull()
+ }
+
+ override suspend fun getPrices(quote: String): List {
+ return webClient.get()
+ .uri("$walletBaseUrl/otc/currency/price?unit=$quote")
+ .header("Content-Type", "application/json")
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono>()
+ .awaitFirst()
+ }
}
\ No newline at end of file
diff --git a/accountant/pom.xml b/accountant/pom.xml
index 15916c8d3..edce57a88 100644
--- a/accountant/pom.xml
+++ b/accountant/pom.xml
@@ -1,5 +1,5 @@
-
4.0.0
@@ -84,8 +84,8 @@
co.nilin.opex.utility
- preferences
- ${preferences.version}
+ interceptors
+ ${interceptor.version}
org.springframework.cloud
diff --git a/api/api-app/Dockerfile b/api/api-app/Dockerfile
index 2adbda7e1..f7086bdca 100644
--- a/api/api-app/Dockerfile
+++ b/api/api-app/Dockerfile
@@ -1,4 +1,4 @@
-FROM openjdk:21
+FROM eclipse-temurin:21-jdk
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml
index d27d6e876..07c13842b 100644
--- a/api/api-app/pom.xml
+++ b/api/api-app/pom.xml
@@ -52,27 +52,33 @@
api-binance-rest
- co.nilin.opex.api.ports.postgres
- api-persister-postgres
+ co.nilin.opex.api.ports.opex
+ api-opex-rest
+ 1.0.1-beta.7
- io.springfox
- springfox-boot-starter
- 3.0.0
+ co.nilin.opex.api.ports.postgres
+ api-persister-postgres
+
org.springframework.cloud
spring-cloud-starter-vault-config
-
- co.nilin.opex.utility
- preferences
-
io.micrometer
micrometer-registry-prometheus
runtime
+
+ com.github.vladimir-bukhtoyarov
+ bucket4j-core
+ 8.0.1
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt
index 35df759a8..e1c0b542a 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/ApiApp.kt
@@ -2,10 +2,8 @@ package co.nilin.opex.api.app
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
-import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.ComponentScan
import org.springframework.scheduling.annotation.EnableScheduling
-import springfox.documentation.swagger2.annotations.EnableSwagger2
@SpringBootApplication
@ComponentScan("co.nilin.opex")
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CacheConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CacheConfig.kt
index 5127560d8..564772a2e 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CacheConfig.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CacheConfig.kt
@@ -1,15 +1,39 @@
package co.nilin.opex.api.app.config
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.connection.RedisConnectionFactory
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
+import org.springframework.data.redis.serializer.StringRedisSerializer
@Configuration
@EnableCaching
class CacheConfig {
+ @Bean
+ fun redisTemplate(connectionFactory: RedisConnectionFactory, mapper: ObjectMapper): RedisTemplate {
+ val newMapper = mapper.copy().apply {
+ activateDefaultTyping(mapper.polymorphicTypeValidator, ObjectMapper.DefaultTyping.EVERYTHING)
+ findAndRegisterModules()
+ registerKotlinModule()
+ }
+ return RedisTemplate().apply {
+ setConnectionFactory(connectionFactory)
+ val ser = GenericJackson2JsonRedisSerializer(newMapper)
+ valueSerializer = ser
+ hashValueSerializer = ser
+ keySerializer = StringRedisSerializer()
+ hashKeySerializer = StringRedisSerializer()
+ afterPropertiesSet()
+ }
+ }
+
@Bean
fun apiKeyCacheManager(): CacheManager {
return ConcurrentMapCacheManager("apiKey")
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CorsConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CorsConfig.kt
new file mode 100644
index 000000000..46abeeb1e
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CorsConfig.kt
@@ -0,0 +1,51 @@
+package co.nilin.opex.api.app.config
+
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsWebFilter
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
+
+@Configuration(proxyBeanMethods = false)
+class CorsConfig(
+ @Value("\${app.cors.enabled:false}")
+ private val enabled: Boolean,
+
+ @Value("\${app.cors.allowed-origins}")
+ private val allowedOrigins: String
+) {
+ private val logger = LoggerFactory.getLogger(CorsConfig::class.java)
+
+
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ fun corsWebFilter(): CorsWebFilter {
+ val config = CorsConfiguration().apply {
+ allowedOrigins = if (enabled) {
+ this@CorsConfig.allowedOrigins
+ .split(",")
+ .map { it.trim() }
+ .filter { it.isNotBlank() }
+ } else {
+ emptyList()
+ }
+ allowedOrigins?.forEach {
+ logger.info("Allowed origin: {}", it)
+ }
+ allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
+ allowedHeaders = listOf("*")
+ exposedHeaders = listOf("Location", "Content-Disposition")
+ allowCredentials = false
+ maxAge = 3600
+ }
+
+ val source = UrlBasedCorsConfigurationSource()
+ source.registerCorsConfiguration("/**", config)
+
+ return CorsWebFilter(source)
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt
index c390d73a1..b28ce1e11 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/InitializeService.kt
@@ -1,28 +1,16 @@
package co.nilin.opex.api.app.config
-import co.nilin.opex.api.ports.postgres.dao.SymbolMapRepository
-import co.nilin.opex.api.ports.postgres.model.SymbolMapModel
-import co.nilin.opex.utility.preferences.Preferences
import jakarta.annotation.PostConstruct
-import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.runBlocking
-import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.DependsOn
import org.springframework.stereotype.Component
@Component
@DependsOn("postgresConfig")
-class InitializeService(private val symbolMapRepository: SymbolMapRepository) {
-
- @Autowired
- private lateinit var preferences: Preferences
+class InitializeService {
@PostConstruct
fun init() = runBlocking {
- preferences.markets.map {
- val pair = it.pair ?: "${it.leftSide}_${it.rightSide}"
- val items = it.aliases.map { a -> SymbolMapModel(null, pair, a.key, a.alias) }
- runCatching { symbolMapRepository.saveAll(items).collectList().awaitSingleOrNull() }
- }
+ // Add symbol maps
}
}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiConfig.kt
new file mode 100644
index 000000000..26d82ad6d
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiConfig.kt
@@ -0,0 +1,51 @@
+package co.nilin.opex.api.app.config
+
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.info.Info
+import io.swagger.v3.oas.models.security.SecurityScheme
+import io.swagger.v3.oas.models.servers.Server
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+class OpenApiConfig(
+ @Value("\${app.openapi.server-url:}")
+ private val serverUrl: String
+) {
+
+ @Bean
+ fun opexOpenApi(): OpenAPI {
+ val openApi= OpenAPI()
+ .info(
+ Info()
+ .title("Opex API")
+ .description("OpenAPI documentation for Opex REST APIs.")
+ .version("1.0.1-beta.7")
+ .description("Backend for opex exchange.")
+ )
+ .components(
+ Components()
+ .addSecuritySchemes(
+ "bearerAuth",
+ SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ .description("JWT Bearer token")
+ )
+ )
+ if (serverUrl.isNotBlank()) {
+ openApi.servers(
+ listOf(
+ Server()
+ .url(serverUrl)
+ .description("Public API server")
+ )
+ )
+ }
+ return openApi
+ }
+}
+
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiCorsConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiCorsConfig.kt
new file mode 100644
index 000000000..02150efcf
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/OpenApiCorsConfig.kt
@@ -0,0 +1,46 @@
+package co.nilin.opex.api.app.config
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.Ordered
+import org.springframework.core.annotation.Order
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsWebFilter
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
+
+@Configuration(proxyBeanMethods = false)
+class OpenApiCorsConfig(
+ @Value("\${app.swagger.cors.enabled:false}")
+ private val enabled: Boolean,
+
+ @Value("\${app.swagger.cors.allowed-origins:http://localhost:8110}")
+ private val allowedOrigins: String
+) {
+
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ fun swaggerCorsWebFilter(): CorsWebFilter {
+ val config = CorsConfiguration().apply {
+ allowedOrigins = if (enabled) {
+ this@OpenApiCorsConfig.allowedOrigins
+ .split(",")
+ .map { it.trim() }
+ .filter { it.isNotBlank() }
+ } else {
+ emptyList()
+ }
+
+ allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
+ allowedHeaders = listOf("*")
+ exposedHeaders = listOf("Location", "Content-Disposition")
+ allowCredentials = false
+ maxAge = 3600
+ }
+
+ val source = UrlBasedCorsConfigurationSource()
+ source.registerCorsConfiguration("/**", config)
+
+ return CorsWebFilter(source)
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfig.kt
new file mode 100644
index 000000000..fd9167451
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfig.kt
@@ -0,0 +1,89 @@
+package co.nilin.opex.api.app.config
+
+import co.nilin.opex.api.app.service.RateLimitCoordinatorService
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import co.nilin.opex.common.OpexError
+import org.slf4j.LoggerFactory
+import org.springframework.security.core.context.ReactiveSecurityContextHolder
+import org.springframework.stereotype.Component
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.server.WebFilter
+import org.springframework.web.server.WebFilterChain
+import org.springframework.web.util.pattern.PathPatternParser
+import reactor.core.publisher.Mono
+
+@Component
+class RateLimitConfig(
+ private val rateLimitConfig: RateLimitConfigService,
+ private val coordinator: RateLimitCoordinatorService
+
+) : WebFilter {
+ private val logger = LoggerFactory.getLogger(RateLimitConfig::class.java)
+ private val parser = PathPatternParser()
+
+ override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono {
+
+ val endpoint = rateLimitConfig.getEndpoints()
+ .asSequence()
+ .filter { it.enabled }
+ .filter { it.method.equals(exchange.request.method.name(), true) }
+ .sortedByDescending { it.priority }
+ .firstOrNull { endpoint ->
+ val pattern = parser.parse(endpoint.url)
+ pattern.matches(exchange.request.path)
+ }
+
+ if (endpoint == null) {
+ return chain.filter(exchange)
+ }
+
+ return applyRateLimitIfAuthenticated(
+ exchange,
+ chain,
+ endpoint.groupId
+ )
+ }
+
+ private fun applyRateLimitIfAuthenticated(
+ exchange: ServerWebExchange,
+ chain: WebFilterChain,
+ groupId: Long
+ ): Mono {
+
+ return ReactiveSecurityContextHolder.getContext()
+ .mapNotNull { it.authentication }
+ .filter { it.isAuthenticated }
+ .flatMap { auth ->
+ applyRateLimit(auth.name, exchange, chain, groupId)
+ }
+ .switchIfEmpty(
+ chain.filter(exchange)
+ )
+ }
+
+ private fun applyRateLimit(
+ identity: String,
+ exchange: ServerWebExchange,
+ chain: WebFilterChain,
+ groupId: Long
+ ): Mono {
+
+ val group = rateLimitConfig.getGroup(groupId)
+ ?: return chain.filter(exchange)
+
+ val result = coordinator.check(
+ identity = identity,
+ groupId = groupId,
+ maxRequests = group.requestCount,
+ windowSeconds = group.requestWindowSeconds,
+ apiPath = exchange.request.uri.path,
+ apiMethod = exchange.request.method.name()
+ )
+
+ return if (result.blocked) {
+ throw OpexError.RateLimit.exception()
+ } else {
+ chain.filter(exchange)
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfigLoader.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfigLoader.kt
new file mode 100644
index 000000000..447bbeb4d
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfigLoader.kt
@@ -0,0 +1,21 @@
+package co.nilin.opex.api.app.config
+
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.springframework.boot.context.event.ApplicationReadyEvent
+import org.springframework.context.event.EventListener
+import org.springframework.stereotype.Component
+
+@Component
+class RateLimitConfigLoader(
+ private val rateLimitConfig: RateLimitConfigService
+) {
+ @EventListener(ApplicationReadyEvent::class)
+ fun preload() {
+ CoroutineScope(Dispatchers.Default).launch {
+ rateLimitConfig.loadConfig()
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/SwaggerConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/SwaggerConfig.kt
deleted file mode 100644
index 68723c02a..000000000
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/SwaggerConfig.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-package co.nilin.opex.api.app.config
-
-import org.springframework.beans.factory.annotation.Value
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import org.springframework.security.core.annotation.AuthenticationPrincipal
-import springfox.documentation.builders.ApiInfoBuilder
-import springfox.documentation.builders.OAuthBuilder
-import springfox.documentation.builders.PathSelectors
-import springfox.documentation.builders.RequestParameterBuilder
-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 java.security.Principal
-import java.util.*
-
-@Configuration
-class SwaggerConfig {
- @Value("\${swagger.authUrl}")
- private lateinit var authUrl: String
-
- @Bean
- fun opexApi(): Docket {
- return Docket(DocumentationType.SWAGGER_2)
- .groupName("opex-api")
- .apiInfo(apiInfo())
- .select()
- .paths(PathSelectors.regex("^/api/v3.*"))
- .build()
- .globalRequestParameters(
- Collections.singletonList(
- RequestParameterBuilder()
- .name("content-type")
- .description("content-type")
- .`in`(ParameterType.HEADER)
- .required(true)
- .build()
- )
- )
- .ignoredParameterTypes(AuthenticationPrincipal::class.java, Principal::class.java)
- .useDefaultResponseMessages(false)
- .securitySchemes(Collections.singletonList(oauth()))
- .securityContexts(Collections.singletonList(securityContext()))
- }
-
- private fun apiInfo(): ApiInfo {
- return ApiInfoBuilder()
- .title("OPEX API")
- .description("Backend for opex exchange.")
- .license("MIT License")
- .licenseUrl("https://github.com/opexdev/Back-end/blob/feature/1-MVP/LICENSE")
- .version("0.1")
- .build()
- }
-
- private fun oauth(): SecurityScheme {
- return OAuthBuilder()
- .name("opex")
- .grantTypes(grantTypes())
- .scopes(scopes())
- .build()
- }
-
- private fun scopes(): List {
- return listOf(AuthorizationScope("openid", "OpenId"))
- }
-
- private fun grantTypes(): List {
- val grantType = ResourceOwnerPasswordCredentialsGrant(authUrl)
- return Collections.singletonList(grantType)
- }
-
- private fun securityContext(): SecurityContext {
- val securityReference = SecurityReference.builder()
- .reference("opex")
- .scopes(emptyArray())
- .build()
- return SecurityContext.builder()
- .securityReferences(Collections.singletonList(securityReference))
- .operationSelector { true }
- .build()
- }
-
- @Bean
- fun securityInfo(): SecurityConfiguration {
- return SecurityConfigurationBuilder.builder()
- .clientId("admin-cli")
- .realm("opex")
- .appName("opex")
- .scopeSeparator(",")
- .build()
- }
-}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt
new file mode 100644
index 000000000..a89032080
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/WebClientConfig.kt
@@ -0,0 +1,98 @@
+package co.nilin.opex.api.app.config
+
+import co.nilin.opex.common.utils.LanguageUtils.getDefaultUserLanguage
+import io.netty.channel.ChannelOption
+import io.netty.handler.logging.LogLevel
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.cloud.client.ServiceInstance
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer
+import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.client.reactive.ReactorClientHttpConnector
+import org.springframework.web.reactive.function.client.ClientRequest
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction
+import org.springframework.web.reactive.function.client.WebClient
+import org.zalando.logbook.Logbook
+import org.zalando.logbook.netty.LogbookClientHandler
+import reactor.core.publisher.Mono
+import reactor.netty.http.client.HttpClient
+import reactor.netty.resources.ConnectionProvider
+import reactor.netty.transport.logging.AdvancedByteBufFormat
+import java.time.Duration
+
+@Configuration
+class WebClientConfig(
+ private val logbook: Logbook,
+ @Value("\${app.auth.url}")
+ private val url: String,
+) {
+ private val provider = ConnectionProvider.builder("apiPool")
+ .maxConnections(150)
+ .pendingAcquireMaxCount(100)
+ .maxIdleTime(Duration.ofSeconds(30))
+ .maxLifeTime(Duration.ofMinutes(2))
+ .pendingAcquireTimeout(Duration.ofSeconds(10))
+ .evictInBackground(Duration.ofMinutes(1))
+ .build()
+
+ private val client = HttpClient.create(provider)
+ .wiretap("reactor.netty.http.client.HttpClient", LogLevel.DEBUG, AdvancedByteBufFormat.SIMPLE)
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
+ .responseTimeout(Duration.ofSeconds(30))
+ .keepAlive(true)
+ .doOnConnected { it.addHandlerLast(LogbookClientHandler(logbook)) }
+
+
+ @Bean("generalWebClient")
+ fun loadBalancedWebClient(
+ loadBalancerFactory: ReactiveLoadBalancer.Factory,
+
+ ): WebClient {
+ return WebClient.builder()
+ .filter(ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, emptyList()))
+ .clientConnector(ReactorClientHttpConnector(client))
+ .filter(languageFilter())
+ .codecs { configurer ->
+ configurer.defaultCodecs()
+ .maxInMemorySize(5 * 1024 * 1024)
+ }
+ .build()
+ }
+
+ @Bean("keycloakWebClient")
+ fun keycloakWebClient(logbook: Logbook): WebClient {
+ val provider = ConnectionProvider.builder("keycloakPool")
+ .maxConnections(100)
+ .maxIdleTime(Duration.ofSeconds(30))
+ .maxLifeTime(Duration.ofMinutes(2))
+ .pendingAcquireTimeout(Duration.ofSeconds(60))
+ .evictInBackground(Duration.ofMinutes(1))
+ .build()
+
+ val client = HttpClient.create(provider)
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
+ .responseTimeout(Duration.ofSeconds(10))
+ .keepAlive(true)
+ .doOnConnected { it.addHandlerLast(LogbookClientHandler(logbook)) }
+
+ client.warmup().block()
+
+ return WebClient.builder()
+ .clientConnector(ReactorClientHttpConnector(client))
+ .baseUrl(url)
+ .build()
+ }
+
+ private fun languageFilter() = ExchangeFilterFunction { request, next ->
+ Mono.deferContextual { ctx ->
+ val lang = ctx.getOrDefault("lang", getDefaultUserLanguage())
+ val mutatedRequest = ClientRequest.from(request)
+ .header("Accept-Language", lang)
+ .build()
+ next.exchange(mutatedRequest)
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt
index ab8bcd624..5bf8beadf 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt
@@ -1,64 +1,339 @@
package co.nilin.opex.api.app.controller
-import co.nilin.opex.api.app.data.APIKeyResponse
-import co.nilin.opex.api.app.data.CreateAPIKeyRequest
-import co.nilin.opex.api.app.service.APIKeyServiceImpl
-import co.nilin.opex.api.ports.binance.util.jwtAuthentication
-import co.nilin.opex.api.ports.binance.util.tokenValue
-import org.springframework.security.core.annotation.CurrentSecurityContext
-import org.springframework.security.core.context.SecurityContext
-import org.springframework.web.bind.annotation.DeleteMapping
-import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PathVariable
-import org.springframework.web.bind.annotation.PostMapping
-import org.springframework.web.bind.annotation.PutMapping
-import org.springframework.web.bind.annotation.RequestBody
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RestController
-import java.security.Principal
+import co.nilin.opex.api.app.data.ApiKeyResponse
+import co.nilin.opex.api.app.data.CreateApiKeyRequest
+import co.nilin.opex.api.app.data.UpdateApiKeyRequest
+import co.nilin.opex.common.security.JwtUtils
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import io.swagger.v3.oas.annotations.enums.ParameterIn
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.web.bind.annotation.*
+import java.security.SecureRandom
+import java.util.*
@RestController
@RequestMapping("/v1/api-key")
-class APIKeyController(private val apiKeyService: APIKeyServiceImpl) {
+@Tag(
+ name = "API App - API Keys",
+ description = "API key management operations."
+)
+class APIKeyController(
+ private val apiKeyService: co.nilin.opex.api.core.spi.APIKeyService
+) {
+ private val rng = SecureRandom()
- @GetMapping
- suspend fun getKeys(principal: Principal): List {
- return apiKeyService.getKeysByUserId(principal.name)
- .map { APIKeyResponse(it.label, it.expirationTime, it.allowedIPs, it.key, it.isEnabled) }
+ private fun generateSecretBase64(bytes: Int = 48): String {
+ val b = ByteArray(bytes)
+ rng.nextBytes(b)
+ return Base64.getEncoder().encodeToString(b)
}
+ private fun canonicalTemplate(): String = "METHOD\nPATH\nQUERY\nBODY_SHA256\nTIMESTAMP_MS"
+
+ private fun headersTemplate(apiKeyId: String): Map = mapOf(
+ "X-API-KEY" to apiKeyId,
+ "X-API-SIGNATURE" to "Base64(HMAC-SHA256(secret, canonical))",
+ "X-API-TIMESTAMP" to "",
+ "X-API-BODY-SHA256" to " (optional)"
+ )
+
@PostMapping
+ @Operation(
+ summary = "Create API key",
+ description = """POST /v1/api-key.
+Security: Bearer user-token required.
+
+Behavior: Creates a new API key for the authenticated user. The generated secret is returned only once in this response.
+Source of values: Use the Bearer token of the user who should own the API key.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
suspend fun create(
- @RequestBody request: CreateAPIKeyRequest,
- @CurrentSecurityContext securityContext: SecurityContext
- ): Any {
- val jwt = securityContext.jwtAuthentication()
- val response = apiKeyService.createAPIKey(
- jwt.name,
- request.label,
- request.expiration?.getLocalDateTime(),
- request.allowedIPs,
- jwt.tokenValue()
+ @Parameter(hidden = true)
+ @RequestHeader(name = "Authorization", required = false)
+ authorization: String?,
+ @RequestBody req: CreateApiKeyRequest
+ ): ApiKeyResponse {
+ require(!authorization.isNullOrBlank() && authorization.startsWith("Bearer ")) { "Authorization Bearer user token is required" }
+ val userToken = authorization.substringAfter("Bearer ").trim()
+ val (userId, preferredUsername) = parseJwtUser(userToken)
+ val apiKeyId = req.apiKeyId?.takeIf { it.isNotBlank() } ?: UUID.randomUUID().toString()
+ val secret = generateSecretBase64()
+ val stored = apiKeyService.createApiKeyRecord(
+ apiKeyId = apiKeyId,
+ label = req.label,
+ plaintextSecret = secret,
+ allowedIps = req.allowedIps,
+ allowedEndpoints = req.allowedEndpoints,
+ keycloakUserId = userId,
+ keycloakUsername = preferredUsername,
+ enabled = true
+ )
+ return ApiKeyResponse(
+ apiKeyId = apiKeyId,
+ label = stored.record.label,
+ enabled = stored.record.enabled,
+ allowedIps = stored.record.allowedIps,
+ allowedEndpoints = stored.record.allowedEndpoints,
+ keycloakUsername = stored.record.keycloakUsername,
+ secret = secret
)
- return object {
- val apiKey = response.second.key
- val secret = response.first
- }
}
- @PutMapping("/{key}/enable")
- suspend fun enableKey(principal: Principal, @PathVariable key: String) {
- apiKeyService.changeKeyState(principal.name, key, true)
+ private fun parseJwtUser(token: String): Pair {
+ val payload = JwtUtils.decodePayload(token)
+ val sub = payload["sub"] as? String
+ val preferred = payload["username"] as? String
+ require(!sub.isNullOrBlank()) { "JWT missing sub" }
+ return Pair(sub!!, preferred)
+ }
+
+ @GetMapping
+ @Operation(
+ summary = "List API keys",
+ description = """GET /v1/api-key.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Returns API key metadata. Secrets are not returned.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ array = ArraySchema(schema = Schema(implementation = ApiKeyResponse::class))
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun list(): List = apiKeyService.listApiKeyRecords().stream().map {
+ ApiKeyResponse(
+ apiKeyId = it.apiKeyId,
+ label = it.label,
+ enabled = it.enabled,
+ allowedIps = it.allowedIps,
+ allowedEndpoints = it.allowedEndpoints,
+ keycloakUsername = it.keycloakUsername,
+ secret = null
+ )
+ }.toList()
+
+ @GetMapping("/{apiKeyId}")
+ @Operation(
+ summary = "Get API key",
+ description = """GET /v1/api-key/{apiKeyId}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Returns API key metadata. Secret is not returned.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun get(@PathVariable apiKeyId: String): ApiKeyResponse {
+ val it = apiKeyService.getApiKeyRecord(apiKeyId) ?: throw NoSuchElementException("API key not found: $apiKeyId")
+ return ApiKeyResponse(
+ apiKeyId = it.apiKeyId,
+ label = it.label,
+ enabled = it.enabled,
+ allowedIps = it.allowedIps,
+ allowedEndpoints = it.allowedEndpoints,
+ keycloakUsername = it.keycloakUsername,
+ secret = null
+ )
}
- @PutMapping("/{key}/disable")
- suspend fun disableKey(principal: Principal, @PathVariable key: String) {
- apiKeyService.changeKeyState(principal.name, key, false)
+ @PostMapping("/{apiKeyId}/rotate")
+ @Operation(
+ summary = "Rotate API key secret",
+ description = """POST /v1/api-key/{apiKeyId}/rotate.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Rotates the API key secret. The new secret is returned only once in this response.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun rotate(@PathVariable apiKeyId: String): ApiKeyResponse {
+ val newSecret = generateSecretBase64()
+ val stored = apiKeyService.rotateApiKeySecret(apiKeyId, newSecret)
+ return ApiKeyResponse(
+ apiKeyId = stored.record.apiKeyId,
+ label = stored.record.label,
+ enabled = stored.record.enabled,
+ allowedIps = stored.record.allowedIps,
+ allowedEndpoints = stored.record.allowedEndpoints,
+ keycloakUsername = stored.record.keycloakUserId,
+ secret = newSecret
+ )
}
- @DeleteMapping("/{key}")
- suspend fun deleteKey(principal: Principal, @PathVariable key: String) {
- apiKeyService.deleteKey(principal.name, key)
+ @PutMapping("/{apiKeyId}")
+ @Operation(
+ summary = "Update API key",
+ description = """PUT /v1/api-key/{apiKeyId}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Updates API key metadata and enabled status. Secret is not returned.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response.",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = ApiKeyResponse::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun update(@PathVariable apiKeyId: String, @RequestBody req: UpdateApiKeyRequest): ApiKeyResponse {
+ val s = apiKeyService.updateApiKeyRecord(
+ apiKeyId = apiKeyId,
+ label = req.label,
+ enabled = req.enabled,
+ allowedIps = req.allowedIps,
+ allowedEndpoints = req.allowedEndpoints,
+ keycloakUsername = req.keycloakUsername
+ )
+ return ApiKeyResponse(
+ apiKeyId = s.apiKeyId,
+ label = s.label,
+ enabled = s.enabled,
+ allowedIps = s.allowedIps,
+ allowedEndpoints = s.allowedEndpoints,
+ keycloakUsername = s.keycloakUserId
+ )
}
-}
\ No newline at end of file
+ @DeleteMapping("/{apiKeyId}")
+ @Operation(
+ summary = "Delete API key",
+ description = """DELETE /v1/api-key/{apiKeyId}.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Deletes or revokes the API key.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ parameters = [Parameter(
+ name = "apiKeyId",
+ `in` = ParameterIn.PATH,
+ required = true,
+ description = "API key id."
+ )],
+ responses = [
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful response. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.",
+ content = [Content()]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Forbidden. Required authority is missing: ROLE_admin. No response body.",
+ content = [Content()]
+ )
+ ]
+ )
+ suspend fun delete(@PathVariable apiKeyId: String) {
+ apiKeyService.deleteApiKeyRecord(apiKeyId)
+ }
+}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
new file mode 100644
index 000000000..b029997e1
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
@@ -0,0 +1,42 @@
+package co.nilin.opex.api.app.controller
+
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.security.SecurityRequirement
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/v1/rate-limit")
+@Tag(
+ name = "API App - Rate Limit",
+ description = "Rate-limit administration operations."
+)
+class RateLimitController(
+ private val rateLimitConfig: RateLimitConfigService,
+) {
+
+ @GetMapping
+ @Operation(
+ summary = "Reload rate-limit config",
+ description = """POST /v1/rate-limit.
+Security: Bearer admin-token required. Required authority: ROLE_admin.
+
+Behavior: Reloads rate-limit configuration from the configured source.
+Response body: No response body.""",
+ security = [SecurityRequirement(name = "bearerAuth")],
+ responses = [
+ ApiResponse(responseCode = "200", description = "Successful response. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "401", description = "Unauthorized. Bearer token is missing, invalid, or expired. No response body.", content = [Content()]),
+ ApiResponse(responseCode = "403", description = "Forbidden. Required authority is missing: ROLE_admin. No response body.", content = [Content()])
+ ]
+ )
+ suspend fun reloadRateLimits() {
+ rateLimitConfig.loadConfig()
+ }
+}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt
index dfe733f83..21da2279f 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt
@@ -1,12 +1,13 @@
package co.nilin.opex.api.app.data
import java.time.LocalDateTime
-import java.util.*
-data class APIKeyResponse(
- val label: String,
- val expirationTime: LocalDateTime?,
- val allowedIPs: String?,
- val key: String,
- val enabled: Boolean
+data class ApiKeyResponse(
+ val apiKeyId: String,
+ val label: String?,
+ val enabled: Boolean,
+ val allowedIps: Set?,
+ val allowedEndpoints: Set?,
+ val keycloakUsername: String?,
+ val secret: String? = null, // only present on create/rotate
)
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/AccessTokenResponse.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/AccessTokenResponse.kt
index e0846aa41..7fe91e449 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/AccessTokenResponse.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/AccessTokenResponse.kt
@@ -2,6 +2,6 @@ package co.nilin.opex.api.app.data
data class AccessTokenResponse(
val access_token: String,
- val refresh_token: String,
+ val refresh_token: String?,
val expires_in: Long
)
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/BlockResult.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/BlockResult.kt
new file mode 100644
index 000000000..a84dd5e73
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/BlockResult.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.app.data
+
+data class BlockResult(
+ val blocked: Boolean,
+ val retryAfterSeconds: Int = 0
+)
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt
index e84994737..20ca10712 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt
@@ -1,7 +1,9 @@
package co.nilin.opex.api.app.data
-data class CreateAPIKeyRequest(
- val label: String,
- val expiration: APIKeyExpiration?,
- val allowedIPs: String?
+data class CreateApiKeyRequest(
+ val apiKeyId: String?,
+ val label: String?,
+ val allowedIps: Set?,
+ val allowedEndpoints: Set?,
+ val keycloakUsername: String?
)
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/RateLimitPenaltyState.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/RateLimitPenaltyState.kt
new file mode 100644
index 000000000..e7d1f5f94
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/RateLimitPenaltyState.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.app.data
+
+data class RateLimitPenaltyState(
+ var violationCount: Int = 0,
+ var lastViolationAt: Long? = null,
+ var bannedUntil: Long? = null
+)
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/RateLimitState.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/RateLimitState.kt
new file mode 100644
index 000000000..15172b495
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/RateLimitState.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.app.data
+
+data class RateLimitState(
+ var violationCount: Int = 0,
+ var blockedUntil: Long? = null,
+ var lastViolationAt: Long? = null,
+ var graceRemaining: Int = 0
+)
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/UpdateApiKeyRequest.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/UpdateApiKeyRequest.kt
new file mode 100644
index 000000000..52bfcfbbc
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/UpdateApiKeyRequest.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.app.data
+
+
+
+data class UpdateApiKeyRequest(
+ val label: String?,
+ val enabled: Boolean?,
+ val allowedIps: Set?,
+ val allowedEndpoints: Set?,
+ val keycloakUsername: String?
+)
+
+
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/impl/ManualRateLimiterImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/impl/ManualRateLimiterImpl.kt
new file mode 100644
index 000000000..40d6b9f0d
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/impl/ManualRateLimiterImpl.kt
@@ -0,0 +1,35 @@
+package co.nilin.opex.api.app.impl
+
+import co.nilin.opex.api.app.service.RateLimitCoordinatorService
+import co.nilin.opex.api.core.inout.ManualRateLimitGroupType
+import co.nilin.opex.api.core.spi.ManualRateLimiterService
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import co.nilin.opex.common.OpexError
+import org.springframework.stereotype.Component
+import org.springframework.web.server.ServerWebExchange
+
+@Component
+class ManualRateLimiterImpl(
+ private val rateLimitConfig: RateLimitConfigService,
+ private val coordinator: RateLimitCoordinatorService
+) : ManualRateLimiterService {
+
+ override fun check(
+ identity: String,
+ group: ManualRateLimitGroupType,
+ exchange: ServerWebExchange
+ ) {
+ val group = rateLimitConfig.getGroup(group.name) ?: return
+ val result = coordinator.check(
+ identity = identity,
+ groupId = group.id!!,
+ maxRequests = group.requestCount,
+ windowSeconds = group.requestWindowSeconds,
+ apiPath = exchange.request.uri.path,
+ apiMethod = exchange.request.method.name()
+ )
+ if (result.blocked) {
+ throw OpexError.RateLimit.exception()
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt
index f015a0ffd..6b505346d 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt
@@ -1,8 +1,13 @@
package co.nilin.opex.api.app.interceptor
-import co.nilin.opex.api.app.service.APIKeyServiceImpl
+import co.nilin.opex.api.app.security.ClientCredentialsTokenService
+import co.nilin.opex.api.app.security.HmacVerifier
+import co.nilin.opex.api.core.spi.APIKeyService
import co.nilin.opex.api.core.spi.APIKeyFilter
-import kotlinx.coroutines.runBlocking
+import co.nilin.opex.common.OpexError
+import kotlinx.coroutines.reactor.mono
+import org.slf4j.LoggerFactory
+import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
@@ -10,25 +15,118 @@ import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
@Component
-class APIKeyFilterImpl(private val apiKeyService: APIKeyServiceImpl) : APIKeyFilter, WebFilter {
+class APIKeyFilterImpl(
+ private val apiKeyService: APIKeyService,
+ private val hmacVerifier: HmacVerifier,
+ private val clientTokenService: ClientCredentialsTokenService
+) : APIKeyFilter, WebFilter {
+
+ private val logger = LoggerFactory.getLogger(APIKeyFilterImpl::class.java)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono {
val request = exchange.request
- val key = request.headers["X-API-KEY"]
- if (!key.isNullOrEmpty()) {
- val secret = request.headers["X-API-SECRET"]
- if (secret.isNullOrEmpty())
- return chain.filter(exchange)
- val apiKey = runBlocking { apiKeyService.getAPIKey(key[0], secret[0]) }
- if (apiKey != null && apiKey.isEnabled && apiKey.accessToken != null && !apiKey.isExpired) {
- val req = exchange.request.mutate()
- .header("Authorization", "Bearer ${apiKey.accessToken}")
- .build()
- return chain.filter(exchange.mutate().request(req).build())
+ val apiKeyId = request.headers["X-API-KEY"]?.firstOrNull()
+ val signature = request.headers["X-API-SIGNATURE"]?.firstOrNull()
+ val tsHeader = request.headers["X-API-TIMESTAMP"]?.firstOrNull()
+ val uri = request.uri
+ val path = "/api" + uri.rawPath
+
+ // HMAC path when signature present
+ if (!apiKeyId.isNullOrBlank() && !signature.isNullOrBlank() && !tsHeader.isNullOrBlank()) {
+ return mono {
+ val entry = apiKeyService.getApiKeyForVerification(apiKeyId)
+ if (entry == null || !entry.enabled) {
+ logger.warn("Unknown or disabled API key: {}", apiKeyId)
+ null
+ } else {
+ // Optional IP allowlist
+ val sourceIp = request.remoteAddress?.address?.hostAddress
+ if (!entry.allowedIps.isNullOrEmpty() && (sourceIp == null || !entry.allowedIps!!.contains(sourceIp))) {
+ logger.warn("API key {} request from disallowed IP {}", apiKeyId, sourceIp)
+ throw OpexError.Forbidden.exception()
+ }
+ if (!entry.allowedEndpoints.isNullOrEmpty() && ( !entry.allowedEndpoints!!.contains(path))) {
+ logger.warn("API key {} request to unauthorized resource {}", apiKeyId, path)
+ throw OpexError.Forbidden.exception()
+ } else {
+ val ts = tsHeader.toLongOrNull()
+ val bodyHash = request.headers["X-API-BODY-SHA256"]?.firstOrNull()
+ if (ts == null) {
+ logger.warn("Invalid timestamp header for bot {}", apiKeyId)
+ throw OpexError.InvalidTime.exception()
+ } else {
+ val ok = hmacVerifier.verify(
+ entry.secret,
+ signature,
+ HmacVerifier.VerificationInput(
+ method = request.method.name(),
+ path = path,
+ query = uri.rawQuery,
+ timestampMillis = ts,
+ bodySha256 = bodyHash
+ )
+ )
+ if (!ok) {
+ logger.warn("Invalid signature for apiKey {}", apiKeyId)
+ throw OpexError.InvalidSignature.exception()
+ } else {
+ val userId = entry.keycloakUserId
+ if (userId.isNullOrBlank()) {
+ logger.warn("API key {} has no mapped Keycloak userId; rejecting", apiKeyId)
+ throw OpexError.UnAuthorized.exception()
+ } else {
+ val bearer = clientTokenService.exchangeToUserToken(userId)
+ val req = request.mutate()
+ .header("Authorization", "Bearer $bearer")
+ .build()
+ exchange.mutate().request(req).build()
+ }
+ }
+ }
+ }
+ }
+ }.flatMap { updatedExchange ->
+ if (updatedExchange != null) chain.filter(updatedExchange) else chain.filter(exchange)
}
}
- return chain.filter(exchange)
- }
+ // Secret-only path with X-API-SECRET (kept as requested). We validate the provided secret
+ // against the stored HMAC secret for the apiKey, then proceed to exchange to the mapped user token.
+ val legacySecret = request.headers["X-API-SECRET"]?.firstOrNull()
+ if (apiKeyId.isNullOrBlank() || legacySecret.isNullOrBlank()) {
+ return chain.filter(exchange)
+ }
+ return mono {
+ val entry = apiKeyService.getApiKeyForVerification(apiKeyId)
+ if (entry == null || !entry.enabled) {
+ logger.warn("Unknown or disabled API key on secret path: {}", apiKeyId)
+ null
+ } else {
+ // Optional IP allowlist
+ val sourceIp = request.remoteAddress?.address?.hostAddress
+ if (!entry.allowedIps.isNullOrEmpty() && (sourceIp == null || !entry.allowedIps!!.contains(sourceIp))) {
+ logger.warn("API key {} request from disallowed IP {} (secret path)", apiKeyId, sourceIp)
+ null
+ } else if (legacySecret != entry.secret) {
+ logger.warn("Invalid X-API-SECRET for apiKey {}", apiKeyId)
+ null
+ } else {
+ val userId = entry.keycloakUserId
+ if (userId.isNullOrBlank()) {
+ logger.warn("API key {} has no mapped Keycloak userId; rejecting (secret path)", apiKeyId)
+ null
+ } else {
+ val bearer = clientTokenService.exchangeToUserToken(userId)
+ val req = request.mutate()
+ .header("Authorization", "Bearer $bearer")
+ .build()
+ exchange.mutate().request(req).build()
+ }
+ }
+ }
+ }.flatMap { updatedExchange ->
+ if (updatedExchange != null) chain.filter(updatedExchange) else chain.filter(exchange)
+ }
+ }
}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt
index 5de6ae557..f39c3301f 100644
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt
@@ -3,6 +3,7 @@ package co.nilin.opex.api.app.proxy
import co.nilin.opex.api.app.data.AccessTokenResponse
import kotlinx.coroutines.reactor.awaitSingle
import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
@@ -12,9 +13,10 @@ import org.springframework.web.reactive.function.client.bodyToMono
@Component
class AuthProxy(
- private val client: WebClient,
@Value("\${app.auth.token-url}")
- private val tokenUrl: String
+ private val tokenUrl: String,
+ @Qualifier("keycloakWebClient")
+ private val client: WebClient
) {
private val logger = LoggerFactory.getLogger(AuthProxy::class.java)
@@ -55,4 +57,50 @@ class AuthProxy(
.bodyToMono()
.awaitSingle()
}
+
+ suspend fun clientCredentials(clientId: String, clientSecret: String, scope: String? = null): AccessTokenResponse {
+ val form = BodyInserters.fromFormData("client_id", clientId)
+ .with("client_secret", clientSecret)
+ .with("grant_type", "client_credentials")
+ val body = if (scope.isNullOrBlank()) form else form.with("scope", scope)
+
+ logger.info("Request client_credentials token for client {}", clientId)
+ return client.post()
+ .uri(tokenUrl)
+ .accept(MediaType.APPLICATION_JSON)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .body(body)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitSingle()
+ }
+
+ // Exchange a client_credentials access token to a user access token (Token Exchange)
+ suspend fun exchangeToUser(
+ clientId: String,
+ clientSecret: String,
+ subjectToken: String,
+ requestedSubjectUserId: String,
+ audience: String? = null
+ ): AccessTokenResponse {
+ val form = BodyInserters.fromFormData("client_id", clientId)
+ .with("client_secret", clientSecret)
+ .with("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
+ .with("subject_token", subjectToken)
+ .with("requested_subject", requestedSubjectUserId)
+ .with("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
+ val body = if (audience.isNullOrBlank()) form else form.with("audience", audience)
+
+ logger.info("Token exchange to user {} via client {}", requestedSubjectUserId, clientId)
+ return client.post()
+ .uri(tokenUrl)
+ .accept(MediaType.APPLICATION_JSON)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .body(body)
+ .retrieve()
+ .onStatus({ t -> t.isError }, { it.createException() })
+ .bodyToMono()
+ .awaitSingle()
+ }
}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ApiKeyRegistry.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ApiKeyRegistry.kt
new file mode 100644
index 000000000..2a7e8662e
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ApiKeyRegistry.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.app.security
+
+interface ApiKeyRegistry {
+ data class BotInfo(
+ val apiKeyId: String,
+ val hmacSecret: String,
+ val enabled: Boolean = true,
+ val allowedIps: Set? = null,
+ val keycloakUsername: String? = null
+ )
+
+ fun find(apiKeyId: String): BotInfo?
+}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ApiKeySecretCryptoImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ApiKeySecretCryptoImpl.kt
new file mode 100644
index 000000000..6d8e72be7
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ApiKeySecretCryptoImpl.kt
@@ -0,0 +1,51 @@
+package co.nilin.opex.api.app.security
+
+import co.nilin.opex.api.core.spi.ApiKeySecretCrypto
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import java.security.SecureRandom
+import java.util.Base64
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.spec.GCMParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+@Component
+class ApiKeySecretCryptoImpl(
+ @Value("\${app.api.crypto.key}") private val base64Key: String
+) : ApiKeySecretCrypto {
+ private val key: SecretKey
+ private val rng = SecureRandom()
+ private val logger = LoggerFactory.getLogger(ApiKeySecretCryptoImpl::class.java)
+
+ init {
+ val decoded = Base64.getDecoder().decode(base64Key)
+ require(decoded.size == 16 || decoded.size == 24 || decoded.size == 32) {
+ "app.api.crypto.key must be 128/192/256-bit Base64 key"
+ }
+ key = SecretKeySpec(decoded, "AES")
+ }
+
+ override fun encrypt(plaintext: String): String {
+ val iv = ByteArray(12)
+ rng.nextBytes(iv)
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+ cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(128, iv))
+ val ct = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
+ val ivB64 = Base64.getEncoder().encodeToString(iv)
+ val ctB64 = Base64.getEncoder().encodeToString(ct)
+ return "$ivB64:$ctB64"
+ }
+
+ override fun decrypt(ciphertext: String): String {
+ val parts = ciphertext.split(":")
+ require(parts.size == 2) { "Invalid encrypted secret format" }
+ val iv = Base64.getDecoder().decode(parts[0])
+ val ct = Base64.getDecoder().decode(parts[1])
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+ cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
+ val pt = cipher.doFinal(ct)
+ return String(pt, Charsets.UTF_8)
+ }
+}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ClientCredentialsTokenService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ClientCredentialsTokenService.kt
new file mode 100644
index 000000000..648a40099
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/ClientCredentialsTokenService.kt
@@ -0,0 +1,53 @@
+package co.nilin.opex.api.app.security
+
+import co.nilin.opex.api.app.proxy.AuthProxy
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.time.Instant
+
+@Service
+class ClientCredentialsTokenService(
+ private val authProxy: AuthProxy,
+ @Value("\${app.auth.api-key-client.id}")
+ private val clientId: String,
+ @Value("\${app.auth.api-key-client.secret}")
+ private val clientSecret: String
+) {
+ private val logger = LoggerFactory.getLogger(ClientCredentialsTokenService::class.java)
+
+ private data class CachedToken(val token: String, val expiresAtMillis: Long)
+
+ // Cache for client_credentials token
+ @Volatile private var cache: CachedToken? = null
+
+ // Cache for exchanged user tokens per subject (exactly like getBearerToken, keyed only by subject)
+ private val subjectCache = java.util.concurrent.ConcurrentHashMap()
+
+ suspend fun getBearerToken(): String {
+ val now = Instant.now().toEpochMilli()
+ val snap = cache
+ if (snap != null && snap.expiresAtMillis - 30_000 > now) return snap.token
+ val resp = authProxy.clientCredentials(clientId, clientSecret)
+ val expiresAt = now + (resp.expires_in * 1000L)
+ val bearer = resp.access_token
+ cache = CachedToken(bearer, expiresAt)
+ logger.debug("Fetched new client_credentials token; expires at {}", expiresAt)
+ return bearer
+ }
+
+ // Convenience: exchange cached client token for a user access token, cached per subject
+ suspend fun exchangeToUserToken(requestedSubjectUserId: String, audience: String? = null): String {
+ val now = Instant.now().toEpochMilli()
+ val cached = subjectCache[requestedSubjectUserId]
+ if (cached != null && cached.expiresAtMillis - 30_000 > now) return cached.token
+
+ val subjectToken = getBearerToken()
+ val exchanged = authProxy.exchangeToUser(clientId, clientSecret, subjectToken, requestedSubjectUserId, audience)
+ val token = exchanged.access_token
+ val expiresAt = now + (exchanged.expires_in * 1000L)
+ subjectCache[requestedSubjectUserId] = CachedToken(token, expiresAt)
+ logger.debug("Exchanged and cached user token for subject {}; expires at {}", requestedSubjectUserId, expiresAt)
+ return token
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/HmacVerifier.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/HmacVerifier.kt
new file mode 100644
index 000000000..97b6a152c
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/security/HmacVerifier.kt
@@ -0,0 +1,61 @@
+package co.nilin.opex.api.app.security
+
+import java.nio.charset.StandardCharsets
+import java.time.Duration
+import java.time.Instant
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+import java.util.Base64
+import kotlin.math.abs
+
+@org.springframework.stereotype.Component
+class HmacVerifier(
+ private val allowedSkew: Duration = Duration.ofMinutes(5)
+) {
+ data class VerificationInput(
+ val method: String,
+ val path: String,
+ val timestampMillis: Long,
+ val bodySha256: String? = null,
+ val query: String? = null
+ )
+
+ fun verify(secret: String, signatureBase64: String, input: VerificationInput): Boolean {
+ // Check timestamp window
+ val now = Instant.now().toEpochMilli()
+ if (abs(now - input.timestampMillis) > allowedSkew.toMillis()) return false
+
+ val canonical = canonicalString(input)
+ val expected = hmacSha256Base64(secret, canonical)
+ // Constant-time compare
+ return constantTimeEquals(signatureBase64, expected)
+ }
+
+ private fun canonicalString(input: VerificationInput): String {
+ val sb = StringBuilder()
+ sb.append(input.method.uppercase()).append('\n')
+ .append(input.path).append('\n')
+ .append(input.query ?: "").append('\n')
+ .append(input.bodySha256 ?: "").append('\n')
+ .append(input.timestampMillis)
+ return sb.toString()
+ }
+
+ private fun hmacSha256Base64(secret: String, data: String): String {
+ val mac = Mac.getInstance("HmacSHA256")
+ mac.init(SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256"))
+ val raw = mac.doFinal(data.toByteArray(StandardCharsets.UTF_8))
+ return Base64.getEncoder().encodeToString(raw)
+ }
+
+ private fun constantTimeEquals(a: String, b: String): Boolean {
+ val aBytes = a.toByteArray(StandardCharsets.UTF_8)
+ val bBytes = b.toByteArray(StandardCharsets.UTF_8)
+ var result = aBytes.size xor bBytes.size
+ val len = minOf(aBytes.size, bBytes.size)
+ for (i in 0 until len) {
+ result = result or (aBytes[i].toInt() xor bBytes[i].toInt())
+ }
+ return result == 0
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt
deleted file mode 100644
index 034808af0..000000000
--- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-package co.nilin.opex.api.app.service
-
-import co.nilin.opex.api.app.proxy.AuthProxy
-import co.nilin.opex.api.core.inout.APIKey
-import co.nilin.opex.api.core.spi.APIKeyService
-import co.nilin.opex.api.ports.postgres.dao.APIKeyRepository
-import co.nilin.opex.api.ports.postgres.model.APIKeyModel
-import co.nilin.opex.common.OpexError
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.reactive.awaitFirstOrElse
-import kotlinx.coroutines.reactive.awaitFirstOrNull
-import kotlinx.coroutines.reactor.awaitSingle
-import kotlinx.coroutines.reactor.awaitSingleOrNull
-import org.slf4j.LoggerFactory
-import org.springframework.beans.factory.annotation.Value
-import org.springframework.cache.Cache
-import org.springframework.cache.CacheManager
-import org.springframework.stereotype.Service
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.util.*
-import java.util.concurrent.TimeUnit
-import javax.crypto.Cipher
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.SecretKeySpec
-import kotlin.math.log
-
-@Service
-class APIKeyServiceImpl(
- private val apiKeyRepository: APIKeyRepository,
- private val authProxy: AuthProxy,
- private val cacheManager: CacheManager,
- @Value("\${app.auth.api-key-client.secret}")
- private val clientSecret: String
-) : APIKeyService {
-
- private val logger = LoggerFactory.getLogger(APIKeyServiceImpl::class.java)
-
- override suspend fun createAPIKey(
- userId: String,
- label: String,
- expirationTime: LocalDateTime?,
- allowedIPs: String?,
- currentToken: String
- ): Pair {
- if (apiKeyRepository.countByUserId(userId).awaitFirstOrElse { 0 } >= 10)
- throw OpexError.APIKeyLimitReached.exception()
-
- val secret = generateSecret()
- val tokenResponse = authProxy.exchangeToken(clientSecret, currentToken)
- val apiKey = apiKeyRepository.save(
- APIKeyModel(
- null,
- userId,
- label,
- encryptAES(tokenResponse.access_token, secret),
- encryptAES(tokenResponse.refresh_token, secret),
- expirationTime,
- allowedIPs,
- tokenExpiration(tokenResponse.expires_in)
- )
- ).awaitSingle()
-
- return Pair(
- secret,
- with(apiKey) {
- APIKey(userId, label, accessToken, expirationTime, allowedIPs, key, isEnabled, isExpired)
- }
- )
- }
-
- override suspend fun getAPIKey(key: String, secret: String): APIKey? = coroutineScope {
- val apiKey = getFromCache(key)
- ?: apiKeyRepository.findByKey(key).awaitSingleOrNull()?.apply { putCache(this) }
-
- with(apiKey) {
- if (this != null) {
- launch { checkupAPIKey(this@with, secret) }
- APIKey(
- userId,
- label,
- decryptAES(accessToken, secret),
- expirationTime,
- allowedIPs,
- key,
- isEnabled,
- isExpired
- )
- } else
- null
- }
- }
-
- override suspend fun getKeysByUserId(userId: String): List {
- return apiKeyRepository.findAllByUserId(userId).collectList().awaitFirstOrElse { emptyList() }
- .map {
- APIKey(
- it.userId,
- it.label,
- it.accessToken,
- it.expirationTime,
- it.allowedIPs,
- it.key,
- it.isEnabled,
- it.isExpired
- )
- }
- }
-
- override suspend fun changeKeyState(userId: String, key: String, isEnabled: Boolean) {
- val apiKey = apiKeyRepository.findByKey(key).awaitSingleOrNull() ?: throw OpexError.NotFound.exception()
- if (apiKey.userId != userId)
- throw OpexError.Forbidden.exception()
- apiKey.isEnabled = isEnabled
- apiKeyRepository.save(apiKey).awaitSingle()
- }
-
- override suspend fun deleteKey(userId: String, key: String) {
- val apiKey = apiKeyRepository.findByKey(key).awaitSingleOrNull() ?: throw OpexError.NotFound.exception()
- if (apiKey.userId != userId)
- throw OpexError.Forbidden.exception()
- apiKeyRepository.delete(apiKey).awaitFirstOrNull()
- }
-
- private suspend fun checkupAPIKey(apiKey: APIKeyModel, secret: String) {
- if (apiKey.isExpired || !apiKey.isEnabled)
- return
-
- try {
- val now = LocalDateTime.now()
- if (apiKey.expirationTime?.isBefore(now) == true) {
- logger.info("Expiring api key ${apiKey.key}")
- apiKey.isExpired = true
- apiKeyRepository.save(apiKey).awaitSingle().apply { updateCache(this) }
- logger.info("API key ${apiKey.key} is expired")
- return
- }
-
- if (apiKey.tokenExpirationTime.isBefore(now)) {
- logger.info("Refreshing api key ${apiKey.key} token")
- val response = authProxy.refreshToken(clientSecret, decryptAES(apiKey.refreshToken, secret))
- apiKey.apply {
- accessToken = encryptAES(response.access_token, secret)
- tokenExpirationTime = tokenExpiration(response.expires_in)
- }
- apiKeyRepository.save(apiKey).awaitSingle().apply { updateCache(this) }
- logger.info("API key ${apiKey.key} token refreshed")
- }
- } catch (e: Exception) {
- logger.error("Error checking api key ${apiKey.key}", e)
- }
- }
-
- private fun encryptAES(input: String, key: String): String {
- val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
- init(Cipher.ENCRYPT_MODE, SecretKeySpec(key.toByteArray(), "AES"), IvParameterSpec(ByteArray(16)))
- }
- val cipherText = cipher.doFinal(input.toByteArray())
- return Base64.getEncoder().encodeToString(cipherText)
- }
-
- private fun decryptAES(cipherText: String, key: String): String {
- val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
- init(Cipher.DECRYPT_MODE, SecretKeySpec(key.toByteArray(), "AES"), IvParameterSpec(ByteArray(16)))
- }
- val plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText))
- return String(plainText)
- }
-
- private fun generateSecret(length: Int = 32): String {
- val chars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
- return (1..length).map { chars.random() }.joinToString("")
- }
-
- private fun tokenExpiration(expiresInSeconds: Long): LocalDateTime {
- val tokenOffsetTime = Date().time + TimeUnit.SECONDS.toMillis(expiresInSeconds) - TimeUnit.MINUTES.toMillis(10)
- return LocalDateTime.ofInstant(Instant.ofEpochMilli(tokenOffsetTime), ZoneId.systemDefault())
- }
-
- private fun getFromCache(key: String): APIKeyModel? {
- return getCache()?.get(key)?.get() as APIKeyModel?
- }
-
- private fun putCache(apiKey: APIKeyModel) {
- getCache()?.apply {
- putIfAbsent(apiKey.key, apiKey)
- }
- }
-
- private fun updateCache(apiKey: APIKeyModel) {
- getCache()?.apply {
- evict(apiKey.key)
- put(apiKey.key, apiKey)
- }
- }
-
- private fun getCache(): Cache? {
- val cache = cacheManager.getCache("apiKey")
- if (cache == null)
- logger.warn("Could not find cache of apiKey")
- return cache
- }
-
-}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/OpexFilterExceptionHandler.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/OpexFilterExceptionHandler.kt
new file mode 100644
index 000000000..afc37bcf0
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/OpexFilterExceptionHandler.kt
@@ -0,0 +1,36 @@
+package co.nilin.opex.api.app.service
+
+import co.nilin.opex.utility.error.data.OpexException
+import co.nilin.opex.utility.error.spi.ErrorTranslator
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
+import org.springframework.core.annotation.Order
+import org.springframework.http.HttpStatusCode
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Component
+import org.springframework.web.server.ServerWebExchange
+import reactor.core.publisher.Mono
+
+@Component
+@Order(-2)
+class OpexFilterExceptionHandler(
+ private val translator: ErrorTranslator,
+ private val objectMapper: ObjectMapper
+) : ErrorWebExceptionHandler {
+
+ override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono {
+
+ if (ex is OpexException) {
+ return translator.translate(ex).flatMap { error ->
+ exchange.response.statusCode = HttpStatusCode.valueOf(error.status?.value() ?: 500)
+ exchange.response.headers.contentType = MediaType.APPLICATION_JSON
+
+ val bytes = objectMapper.writeValueAsBytes(error)
+ val buffer = exchange.response.bufferFactory().wrap(bytes)
+
+ exchange.response.writeWith(Mono.just(buffer))
+ }
+ }
+ return Mono.error(ex)
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimitCoordinatorService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimitCoordinatorService.kt
new file mode 100644
index 000000000..61744d114
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimitCoordinatorService.kt
@@ -0,0 +1,42 @@
+package co.nilin.opex.api.app.service
+
+import co.nilin.opex.api.app.data.BlockResult
+import org.springframework.stereotype.Component
+
+@Component
+class RateLimitCoordinatorService(
+ private val rateLimiterService: RateLimiterService,
+ private val penaltyService: RateLimitPenaltyService
+) {
+
+
+ fun check(
+ identity: String,
+ groupId: Long,
+ maxRequests: Int,
+ windowSeconds: Int,
+ apiPath: String,
+ apiMethod: String
+ ): BlockResult {
+
+ val blocked = penaltyService.isBlocked(identity, apiPath, apiMethod)
+ if (blocked.blocked) {
+ return blocked
+ }
+
+ val allowed = rateLimiterService.checkRateLimit(
+ identity = identity,
+ maxRequests = maxRequests,
+ windowInSeconds = windowSeconds,
+ apiPath = apiPath,
+ apiMethod = apiMethod
+ )
+
+ return if (allowed) {
+ penaltyService.onAllowed(identity, groupId, apiPath, apiMethod)
+ BlockResult(blocked = false)
+ } else {
+ penaltyService.onLimit(identity, groupId, apiPath, apiMethod)
+ }
+ }
+}
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimitPenaltyService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimitPenaltyService.kt
new file mode 100644
index 000000000..f0d483478
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimitPenaltyService.kt
@@ -0,0 +1,103 @@
+package co.nilin.opex.api.app.service
+
+import co.nilin.opex.api.app.data.BlockResult
+import co.nilin.opex.api.app.data.RateLimitPenaltyState
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import co.nilin.opex.api.ports.postgres.util.RedisCacheHelper
+import co.nilin.opex.common.utils.DynamicInterval
+import org.springframework.stereotype.Component
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+import kotlin.math.min
+
+@Component
+class RateLimitPenaltyService(private val config: RateLimitConfigService, private val redis: RedisCacheHelper) {
+
+ fun isBlocked(identity: String, apiPath: String, apiMethod: String): BlockResult {
+ val state = getPenaltyState(identity, apiPath, apiMethod) ?: return BlockResult(false)
+
+ val now = System.currentTimeMillis()
+ val bannedUntil = state.bannedUntil ?: return BlockResult(false)
+
+ return if (bannedUntil > now) {
+ BlockResult(
+ blocked = true,
+ retryAfterSeconds = ((bannedUntil - now) / 1000).toInt()
+ )
+ } else {
+ BlockResult(false)
+ }
+ }
+
+ fun onLimit(identity: String, groupId: Long, apiPath: String, apiMethod: String): BlockResult {
+ val now = System.currentTimeMillis()
+ val group = config.getGroup(groupId) ?: return BlockResult(false)
+ val penalties = config.getPenalties(groupId).sortedBy { it.blockStep }
+
+ val current = getPenaltyState(identity, apiPath, apiMethod)
+ val nextViolationCount = (current?.violationCount ?: 0) + 1
+
+ val level = min(nextViolationCount, penalties.size)
+ val penalty = penalties[level - 1]
+
+ val bannedUntil = now + Duration.ofSeconds(penalty.blockDurationSeconds.toLong()).toMillis()
+
+ val newState = RateLimitPenaltyState(
+ violationCount = nextViolationCount,
+ lastViolationAt = now,
+ bannedUntil = bannedUntil
+ )
+
+ val ttl = penalty.blockDurationSeconds + group.cooldownSeconds
+
+ savePenaltyState(identity, apiPath, apiMethod, newState, ttl)
+
+ return BlockResult(
+ blocked = true,
+ retryAfterSeconds = penalty.blockDurationSeconds
+ )
+ }
+
+ fun onAllowed(identity: String, groupId: Long, apiPath: String, apiMethod: String) {
+ val state = getPenaltyState(identity, apiPath, apiMethod) ?: return
+ val group = config.getGroup(groupId) ?: return
+ val now = System.currentTimeMillis()
+
+ val lastViolation = state.lastViolationAt ?: return
+ val cooldownMillis = Duration.ofSeconds(group.cooldownSeconds.toLong()).toMillis()
+
+ if (now - lastViolation >= cooldownMillis && state.violationCount > 0) {
+ val newState = state.copy(
+ violationCount = state.violationCount - 1
+ )
+ savePenaltyState(identity, apiPath, apiMethod, newState, group.cooldownSeconds)
+ }
+ }
+
+ private fun getPenaltyState(
+ identity: String,
+ apiPath: String,
+ apiMethod: String
+ ): RateLimitPenaltyState? {
+ return redis.get(buildPenaltyStateKey(identity, apiPath, apiMethod))
+ }
+
+ private fun savePenaltyState(
+ identity: String,
+ apiPath: String,
+ apiMethod: String,
+ state: RateLimitPenaltyState,
+ ttlSeconds: Int
+ ) {
+ redis.put(
+ buildPenaltyStateKey(identity, apiPath, apiMethod),
+ state,
+ DynamicInterval(ttlSeconds, TimeUnit.SECONDS)
+ )
+ }
+
+ private fun buildPenaltyStateKey(identity: String, apiPath: String, apiMethod: String): String {
+ val key = "$identity:$apiMethod:$apiPath"
+ return "rl:penalty:${key.hashCode()}"
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimiterService.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimiterService.kt
new file mode 100644
index 000000000..b39431cbd
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/RateLimiterService.kt
@@ -0,0 +1,53 @@
+package co.nilin.opex.api.app.service
+
+import co.nilin.opex.api.ports.postgres.util.RedisCacheHelper
+import co.nilin.opex.common.utils.DynamicInterval
+import io.github.bucket4j.Bandwidth
+import io.github.bucket4j.Bucket
+import io.github.bucket4j.Refill
+import org.springframework.stereotype.Component
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+
+
+@Component
+class RateLimiterService(
+ private val redisCacheHelper: RedisCacheHelper
+) {
+
+ private val buckets: MutableMap = mutableMapOf()
+
+ private fun createBucket(maxRequests: Int, windowInSeconds: Long): Bucket {
+ val limit = Bandwidth.classic(
+ maxRequests.toLong(),
+ Refill.greedy(maxRequests.toLong(), Duration.ofSeconds(windowInSeconds))
+ )
+ return Bucket.builder().addLimit(limit).build()
+ }
+
+ fun checkRateLimit(identity: String, maxRequests: Int, windowInSeconds: Int, apiPath: String, apiMethod : String): Boolean {
+ val key = "$identity:$apiMethod:$apiPath"
+ val redisKey = "rl:${key.hashCode()}"
+
+ val storedTokenCount: Long? = redisCacheHelper.get(redisKey)
+ val bucket = buckets.computeIfAbsent(redisKey) { createBucket(maxRequests, windowInSeconds.toLong()) }
+
+ if (storedTokenCount == null) {
+ bucket.reset()
+ redisCacheHelper.put(redisKey, maxRequests.toLong(), DynamicInterval(windowInSeconds, TimeUnit.SECONDS))
+ } else {
+ val tokensToAdd = storedTokenCount - bucket.availableTokens
+ if (tokensToAdd > 0) {
+ bucket.addTokens(tokensToAdd)
+ }
+ }
+
+ val allowed = bucket.tryConsume(1)
+
+ if (allowed) {
+ redisCacheHelper.put(redisKey, bucket.availableTokens, DynamicInterval(windowInSeconds, TimeUnit.SECONDS))
+ }
+
+ return allowed
+ }
+}
\ No newline at end of file
diff --git a/api/api-app/src/main/resources/application.yml b/api/api-app/src/main/resources/application.yml
index 3833dc8a4..ec0a0b34f 100644
--- a/api/api-app/src/main/resources/application.yml
+++ b/api/api-app/src/main/resources/application.yml
@@ -10,7 +10,19 @@ spring:
url: r2dbc:postgresql://${DB_IP_PORT:localhost}/opex
username: ${dbusername:opex}
password: ${dbpassword:hiopex}
- initialization-mode: always
+# initialization-mode: always
+ pool:
+ enabled: true
+ initial-size: 5
+ max-size: 20
+ max-idle-time: 60s
+ validation-query: SELECT 1
+ datasource:
+ url: jdbc:postgresql://${DB_IP_PORT:localhost}/opex
+ username: ${dbusername:opex}
+ password: ${dbpassword:hiopex}
+ cache:
+ type: redis
cloud:
bootstrap:
enabled: true
@@ -37,6 +49,10 @@ spring:
prefer-ip-address: true
config:
import: vault://secret/${spring.application.name}
+ data:
+ redis:
+ port: 6379
+ host: redis-cache
management:
endpoints:
web:
@@ -91,9 +107,18 @@ logging:
level:
co.nilin: INFO
org.zalando.logbook: TRACE
+ reactor.netty.pool: DEBUG
+ reactor.netty.http.client: DEBUG
+ org.springframework.web.reactive.function.client: DEBUG
+ co.nilin.opex.api.ports.proxy.impl: DEBUG
+
app:
+ base:
+ url: ${APP_BASE_URL:api}
accountant:
url: http://opex-accountant
+ profile:
+ url: http://opex-profile
matching-gateway:
url: http://opex-matching-gateway
wallet:
@@ -102,12 +127,47 @@ app:
url: http://opex-market
opex-bc-gateway:
url: http://opex-bc-gateway
+ storage:
+ url: http://storage
+ config:
+ url: http://opex-config
+ auth-gateway:
+ url: http://opex-auth-gateway
auth:
- cert-url: http://opex-auth/auth/realms/opex/protocol/openid-connect/certs
- token-url: http://opex-auth/auth/realms/opex/protocol/openid-connect/token
+ url: http://keycloak:8080
+ cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs
+ iss-url: ${TOKEN_ISSUER_URL:http://keycloak:8080/realms/opex}
+ token-url: http://keycloak:8080/realms/opex/protocol/openid-connect/token
api-key-client:
secret: ${API_KEY_CLIENT_SECRET}
+ id: opex-api-key
binance:
api-url: https://api1.binance.com
-swagger:
- authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token
+ user-activity-reference-currency: ${USER_ACTIVITY_REFERENCE_CURRENCY:USDT}
+ custom-message:
+ enabled: ${CUSTOM_MESSAGE_ENABLED:false}
+ base-url: ${CUSTOM_MESSAGE_URL}
+ custom-user-language:
+ enabled: ${CUSTOM_USER_LANGUAGE_ENABLED:false}
+ api:
+ crypto:
+ key: ${api_crypto_key:0e1fd29572ec8c85970d76e3433e96ee}
+ cors:
+ enabled: true
+ allowed-origins: ${ALLOWED_ORIGINS:"http://localhost:8110"}
+ openapi:
+ server-url: ${OPEN_API_SERVER_URL}
+# --- Swagger / SpringDoc (env-driven) ---
+springdoc:
+ api-docs:
+ enabled: ${SWAGGER_API_DOCS_ENABLED:false}
+ path: ${SWAGGER_API_DOCS_PATH:/v3/api-docs}
+ swagger-ui:
+ enabled: ${SWAGGER_UI_ENABLED:false}
+ path: ${SWAGGER_UI_PATH:/swagger-ui.html}
+ try-it-out-enabled: ${SWAGGER_TRY_IT_OUT_ENABLED:true}
+ persist-authorization: ${SWAGGER_PERSIST_AUTHORIZATION:true}
+ display-request-duration: true
+ operations-sorter: method
+ tags-sorter: alpha
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddAddressBookItemRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddAddressBookItemRequest.kt
new file mode 100644
index 000000000..3135d699d
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddAddressBookItemRequest.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.core.inout
+
+data class AddAddressBookItemRequest(
+ var name: String,
+ var address: String,
+ var addressType: String,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddBankAccountRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddBankAccountRequest.kt
new file mode 100644
index 000000000..1f1ea2235
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddBankAccountRequest.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.core.inout
+
+data class AddBankAccountRequest(
+ val name: String? = null,
+ val cardNumber: String? = null,
+ val iban: String? = null,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddressBookResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddressBookResponse.kt
new file mode 100644
index 000000000..2f9975bc6
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AddressBookResponse.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+data class AddressBookResponse(
+ var id: Long? = null,
+ var name: String,
+ var address: String,
+ var addressType: String,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminDepositHistoryRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminDepositHistoryRequest.kt
new file mode 100644
index 000000000..b2489b7b8
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminDepositHistoryRequest.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.api.core.inout
+
+data class AdminDepositHistoryRequest(
+ val uuid: String?,
+ val currency: String?,
+ val sourceAddress: String?,
+ val transactionRef: String?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val status: List? = emptyList(),
+ val ascendingByTime: Boolean = false,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminOrdersHistoryRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminOrdersHistoryRequest.kt
new file mode 100644
index 000000000..b8a68298e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminOrdersHistoryRequest.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+data class AdminOrdersHistoryRequest(
+ val uuid: String?,
+ val symbol: String?,
+ val ouid: String?,
+ val startTime: Long?,
+ val endTime: Long?,
+ val orderType: MatchingOrderType?,
+ val direction: OrderDirection?,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+ val ascendingByTime: Boolean? = false,
+ val includeNames: Boolean? = false
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTradeHistoryRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTradeHistoryRequest.kt
new file mode 100644
index 000000000..ca823b09c
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTradeHistoryRequest.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+data class AdminTradeHistoryRequest(
+ val coin: String?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+ val ascendingByTime: Boolean = false
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTradesHistoryRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTradesHistoryRequest.kt
new file mode 100644
index 000000000..1065682f7
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTradesHistoryRequest.kt
@@ -0,0 +1,20 @@
+package co.nilin.opex.api.core.inout
+
+data class AdminTradesHistoryRequest(
+ val symbol: String?,
+ val baseAsset: String?,
+ val quoteAsset: String?,
+ val ouid: String?,
+ val makerOuid: String?,
+ val takerOuid: String?,
+ val uuid: String?,
+ val makerUuid: String?,
+ val takerUuid: String?,
+ val fromDate: Long?,
+ val toDate: Long?,
+ val ascendingByTime: Boolean? = false,
+ val excludeSelfTrade: Boolean? = true,
+ val limit: Int?=10,
+ val offset: Int?=0,
+ val includeNames: Boolean? = false,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt
new file mode 100644
index 000000000..f4dd5b78f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminTransferReserveRequest.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class AdminTransferReserveRequest(
+ val sourceSymbol: String,
+ val sourceAmount: BigDecimal,
+ val destSymbol: String,
+ val destAmount: BigDecimal? = null,
+ val rate: BigDecimal? = null,
+ val receiverUuid: String,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminWithdrawHistoryRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminWithdrawHistoryRequest.kt
new file mode 100644
index 000000000..7d7a0c073
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AdminWithdrawHistoryRequest.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+data class AdminWithdrawHistoryRequest(
+ val uuid: String? = null,
+ val withdrawUuid: String? = null,
+ val currency: String?= null,
+ val destTxRef: String?= null,
+ val destAddress: String?= null,
+ val status: List = emptyList(),
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val ascendingByTime: Boolean = false,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Amount.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Amount.kt
new file mode 100644
index 000000000..8bf4ce101
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Amount.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class Amount(val currency: CurrencyCommand, val amount: BigDecimal)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignAddressRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignAddressRequest.kt
new file mode 100644
index 000000000..79766d975
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignAddressRequest.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.api.core.inout
+
+data class AssignAddressRequest(val uuid: String, val currency: String, val gatewayUuid: String)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignedAddress.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignedAddress.kt
index 29d26702f..d1eba6729 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignedAddress.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/AssignedAddress.kt
@@ -1,5 +1,7 @@
package co.nilin.opex.api.core.inout
+import java.time.LocalDateTime
+
data class AddressType(val id: Long, val type: String, val addressRegex: String, val memoRegex: String?)
data class Chain(val name: String, val addressTypes: List)
data class AssignedAddress(
@@ -7,5 +9,7 @@ data class AssignedAddress(
val address: String,
val memo: String?,
val type: AddressType,
- val chains: MutableList
+ val chains: MutableList,
+ var expTime: LocalDateTime? = null,
+ var assignedDate: LocalDateTime? = null,
)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountResponse.kt
new file mode 100644
index 000000000..156a92269
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/BankAccountResponse.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.api.core.inout
+
+
+data class BankAccountResponse(
+ var id: Long? = null,
+ var name: String? = null,
+ var cardNumber: String? = null,
+ var iban: String? = null,
+ var accountNumber: String? = null,
+ var bank: String? = null,
+ var status: BankAccountStatus,
+)
+
+enum class BankAccountStatus {
+ WAITING, VERIFIED, REJECTED
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ChainInfo.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ChainInfo.kt
new file mode 100644
index 000000000..e6c0540a6
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ChainInfo.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+data class ChainInfo(
+ val name: String,
+ val addressTypes: String?,
+ val externalChainScannerUrl: String? = null,
+ val addressRegex: String? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CompleteProfileRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CompleteProfileRequest.kt
new file mode 100644
index 000000000..f12be7dcc
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CompleteProfileRequest.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout
+
+data class CompleteProfileRequest(
+ var firstName: String,
+ var lastName: String,
+ var address: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: NationalityType,
+ var identifier: String,
+ var gender: Gender,
+ var birthDate: Long,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ContactUpdateConfirmRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ContactUpdateConfirmRequest.kt
new file mode 100644
index 000000000..78059c4b9
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ContactUpdateConfirmRequest.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.core.inout
+
+data class ContactUpdateConfirmRequest(
+ val email: String? = null,
+ val mobile: String? = null,
+ val otpCode: String,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ContactUpdateRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ContactUpdateRequest.kt
new file mode 100644
index 000000000..90a77978d
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ContactUpdateRequest.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class ContactUpdateRequest(
+ val email: String? = null,
+ val mobile: String? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyCommand.kt
new file mode 100644
index 000000000..db3195096
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyCommand.kt
@@ -0,0 +1,39 @@
+package co.nilin.opex.api.core.inout
+
+
+import java.math.BigDecimal
+import java.util.*
+
+data class CurrencyCommand(
+ var symbol: String,
+ var uuid: String? = UUID.randomUUID().toString(),
+ var name: String? = null,
+ var precision: BigDecimal,
+ var title: String? = null,
+ var alias: String? = null,
+ var icon: String? = null,
+ var isTransitive: Boolean? = false,
+ var isActive: Boolean? = true,
+ var sign: String? = null,
+ var description: String? = null,
+ var shortDescription: String? = null,
+ var withdrawAllowed: Boolean? = false,
+ var depositAllowed: Boolean? = false,
+ var externalUrl: String? = null,
+ var gateways: List? = null,
+ var availableGatewayType: String? = null,
+ var displayOrder: Int? = null
+
+) {
+ fun updateTo(newData: CurrencyCommand): CurrencyCommand {
+ return newData.apply {
+ this.uuid = uuid
+ this.symbol = symbol
+ }
+ }
+
+
+}
+
+
+data class CurrenciesCommand(var currencies: List?)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyData.kt
new file mode 100644
index 000000000..89134d287
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyData.kt
@@ -0,0 +1,24 @@
+package co.nilin.opex.api.core.inout
+
+
+import java.math.BigDecimal
+import java.util.*
+
+data class CurrencyData(
+ var symbol: String,
+ var uuid: String? = UUID.randomUUID().toString(),
+ var name: String? = null,
+ var precision: BigDecimal,
+ var title: String? = null,
+ var alias: String? = null,
+ var icon: String? = null,
+ var isTransitive: Boolean? = false,
+ var isActive: Boolean? = true,
+ var sign: String? = null,
+ var description: String? = null,
+ var shortDescription: String? = null,
+ var externalUrl: String? = null,
+ var displayOrder: Int? = null,
+ var maxOrder: BigDecimal? = null,
+
+ )
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyLocalizationCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyLocalizationCommand.kt
new file mode 100644
index 000000000..91fae9b94
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyLocalizationCommand.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+data class CurrencyLocalizationCommand(
+ var id: Long? = null,
+ var name: String? = null,
+ var title: String? = null,
+ var alias: String? = null,
+ var description: String? = null,
+ var shortDescription: String? = null,
+ var language: String,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyLocalizationResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyLocalizationResponse.kt
new file mode 100644
index 000000000..4bb7bbaa6
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/CurrencyLocalizationResponse.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class CurrencyLocalizationResponse(
+ val currency: String,
+ val currencyLocalizations: List
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositAdminResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositAdminResponse.kt
new file mode 100644
index 000000000..3fcbcd6ad
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositAdminResponse.kt
@@ -0,0 +1,21 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class DepositAdminResponse(
+ val id: String,
+ val uuid: String,
+ val ownerName: String?,
+ val currency: String,
+ val amount: BigDecimal,
+ val network: String?,
+ val note: String?,
+ val transactionRef: String?,
+ val sourceAddress: String?,
+ val status: DepositStatus,
+ val type: DepositType,
+ val attachment: String?,
+ val createDate: LocalDateTime,
+ val transferMethod: TransferMethod?
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositHistoryResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositHistoryResponse.kt
new file mode 100644
index 000000000..3889e771a
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositHistoryResponse.kt
@@ -0,0 +1,30 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.util.*
+
+data class DepositHistoryResponse(
+ val id: String,
+ val uuid: String,
+ val currency: String,
+ val amount: BigDecimal,
+ val network: String?,
+ val note: String?,
+ val transactionRef: String?,
+ val sourceAddress: String?,
+ val status: DepositStatus,
+ val type: DepositType,
+ val attachment: String?,
+ val createDate: Date?,
+ val transferMethod: TransferMethod?
+)
+
+enum class DepositType {
+
+ ON_CHAIN, OFF_CHAIN
+}
+
+enum class DepositStatus {
+
+ PROCESSING, DONE, INVALID
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt
new file mode 100644
index 000000000..90d628b97
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+object DepositWebhookHeaders {
+ const val SIGNATURE = "X-Signature"
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt
new file mode 100644
index 000000000..86d5b07a9
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class DepositWebhookRequest(
+ val referenceNumber: String,
+ val depositNumber: String,
+ val symbol: String,
+ val amount: BigDecimal,
+ val externalIdentifier: String,
+ val date: Long
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt
new file mode 100644
index 000000000..659cc3d3f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.api.core.inout
+
+data class DepositWebhookResponse(
+ val referenceNumber: String? = null,
+ val status: String,
+ val duplicate: Boolean = false,
+ val message: String? = null
+)
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt
new file mode 100644
index 000000000..e7f29715b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class FeeConfig(
+ val name: String,
+ val displayOrder: Int,
+ val minAssetVolume: BigDecimal,
+ val maxAssetVolume: BigDecimal? = null,
+ val minTradeVolume: BigDecimal,
+ val maxTradeVolume: BigDecimal? = null,
+ val makerFee: BigDecimal,
+ val takerFee: BigDecimal,
+ val condition: Condition
+)
+enum class Condition { AND, OR }
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayCommand.kt
new file mode 100644
index 000000000..457deb99a
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayCommand.kt
@@ -0,0 +1,108 @@
+package co.nilin.opex.api.core.inout
+
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import java.math.BigDecimal
+import java.util.*
+
+
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = OffChainGatewayCommand::class, name = "OffChain"),
+ JsonSubTypes.Type(value = OnChainGatewayCommand::class, name = "OnChain"),
+)
+open abstract class CurrencyGatewayCommand(
+ open var currencySymbol: String? = null,
+ open var gatewayUuid: String? = UUID.randomUUID().toString(),
+ open var isDepositActive: Boolean?,
+ open var isWithdrawActive: Boolean?,
+ open var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawAllowed: Boolean? = true,
+ open var depositAllowed: Boolean? = true,
+ open var depositMin: BigDecimal? = BigDecimal.ZERO,
+ open var depositMax: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ open var depositDescription: String? = null,
+ open var withdrawDescription: String? = null,
+ open var displayOrder: Int? = null,
+)
+
+data class OffChainGatewayCommand(
+ var transferMethod: TransferMethod,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isDepositActive: Boolean? = true,
+ override var isWithdrawActive: Boolean? = true,
+ override var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawAllowed: Boolean? = true,
+ override var depositAllowed: Boolean? = true,
+ override var depositMin: BigDecimal? = BigDecimal.ZERO,
+ override var depositMax: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ override var depositDescription: String? = null,
+ override var withdrawDescription: String? = null,
+ override var displayOrder: Int? = null,
+
+ ) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isDepositActive,
+ isWithdrawActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax,
+ depositDescription,
+ withdrawDescription,
+ displayOrder
+)
+
+data class OnChainGatewayCommand(
+
+ var implementationSymbol: String? = null,
+ var tokenName: String? = null,
+ var tokenAddress: String? = null,
+ var isToken: Boolean? = false,
+ var decimal: Int,
+ var chain: String,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isDepositActive: Boolean? = true,
+ override var isWithdrawActive: Boolean? = true,
+ override var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawAllowed: Boolean? = true,
+ override var depositAllowed: Boolean? = true,
+ override var depositMin: BigDecimal? = BigDecimal.ZERO,
+ override var depositMax: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ override var depositDescription: String? = null,
+ override var withdrawDescription: String? = null,
+ override var displayOrder: Int? = null,
+) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isDepositActive,
+ isWithdrawActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax,
+ depositDescription,
+ withdrawDescription,
+ displayOrder
+)
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayLocalizationCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayLocalizationCommand.kt
new file mode 100644
index 000000000..9c147858b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayLocalizationCommand.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout
+
+
+data class GatewayLocalizationCommand(
+ var id: Long? = null,
+ var depositDescription: String? = null,
+ var withdrawDescription: String? = null,
+ var language: String
+)
+
+
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayLocalizationResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayLocalizationResponse.kt
new file mode 100644
index 000000000..79e149333
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayLocalizationResponse.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class GatewayLocalizationResponse(
+ val gatewayUuid: String,
+ val localizations: List
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayUpdateCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayUpdateCommand.kt
new file mode 100644
index 000000000..6378b2c25
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/GatewayUpdateCommand.kt
@@ -0,0 +1,105 @@
+package co.nilin.opex.api.core.inout
+
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import java.math.BigDecimal
+import java.util.*
+
+enum class GatewayType() {
+ OnChain, OffChain
+}
+
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = OffChainGatewayCommand::class, name = "OffChain"),
+ JsonSubTypes.Type(value = OnChainGatewayCommand::class, name = "OnChain"),
+)
+open abstract class CurrencyGatewayUpdateCommand(
+ open var currencySymbol: String? = null,
+ open var gatewayUuid: String? = UUID.randomUUID().toString(),
+ open var isDepositActive: Boolean?,
+ open var isWithdrawActive: Boolean?,
+ open var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawAllowed: Boolean? = true,
+ open var depositAllowed: Boolean? = true,
+ open var depositMin: BigDecimal? = BigDecimal.ZERO,
+ open var depositMax: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ open var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ open var displayOrder: Int? = null,
+)
+
+data class OffChainGatewayUpdateCommand(
+ var transferMethod: TransferMethod,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isDepositActive: Boolean? = true,
+ override var isWithdrawActive: Boolean? = true,
+ override var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawAllowed: Boolean? = true,
+ override var depositAllowed: Boolean? = true,
+ override var depositMin: BigDecimal? = BigDecimal.ZERO,
+ override var depositMax: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ override var displayOrder: Int? = null,
+
+ ) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isDepositActive,
+ isWithdrawActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax,
+ null,
+ null,
+ displayOrder
+)
+
+data class OnChainGatewayUpdateCommand(
+
+ var implementationSymbol: String? = null,
+ var tokenName: String? = null,
+ var tokenAddress: String? = null,
+ var isToken: Boolean? = false,
+ var decimal: Int,
+ var chain: String,
+ override var currencySymbol: String? = null,
+ override var gatewayUuid: String? = UUID.randomUUID().toString(),
+ override var isDepositActive: Boolean? = true,
+ override var isWithdrawActive: Boolean? = true,
+ override var withdrawFee: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawAllowed: Boolean? = true,
+ override var depositAllowed: Boolean? = true,
+ override var depositMin: BigDecimal? = BigDecimal.ZERO,
+ override var depositMax: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMin: BigDecimal? = BigDecimal.ZERO,
+ override var withdrawMax: BigDecimal? = BigDecimal.ZERO,
+ override var displayOrder: Int? = null,
+) : CurrencyGatewayCommand(
+ currencySymbol,
+ gatewayUuid,
+ isDepositActive,
+ isWithdrawActive,
+ withdrawFee,
+ withdrawAllowed,
+ depositAllowed,
+ depositMin,
+ depositMax,
+ withdrawMin,
+ withdrawMax,
+ null,
+ null,
+ displayOrder
+)
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Gender.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Gender.kt
new file mode 100644
index 000000000..04035f4c5
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Gender.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+enum class Gender {
+ FEMALE, MALE
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/InquiryRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/InquiryRequest.kt
new file mode 100644
index 000000000..31afe1413
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/InquiryRequest.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class InquiryRequest(
+ val startTime: Long,
+ val status: List? = null,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/KycLevel.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/KycLevel.kt
new file mode 100644
index 000000000..417268013
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/KycLevel.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+enum class KycLevel {
+ LEVEL_1, LEVEL_2, LEVEL_3
+}
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ManualRateLimitGroupType.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ManualRateLimitGroupType.kt
new file mode 100644
index 000000000..110844a65
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ManualRateLimitGroupType.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+enum class ManualRateLimitGroupType {
+ VERIFY_OTP,
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ManualTransferRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ManualTransferRequest.kt
new file mode 100644
index 000000000..431aafb1f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ManualTransferRequest.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+data class ManualTransferRequest(
+ var ref: String,
+ var description: String? = null,
+ var attachment: String?,
+ var gatewayUuid: String
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/MarketBasicData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/MarketBasicData.kt
new file mode 100644
index 000000000..de478609e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/MarketBasicData.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.core.inout
+
+data class MarketBasicData(
+ val quoteCurrencies: List,
+ val referenceCurrencies: List,
+ val userActivityReferenceCurrency: String
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/NationalityType.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/NationalityType.kt
new file mode 100644
index 000000000..a9eb89bec
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/NationalityType.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+enum class NationalityType {
+ IRANIAN,
+ NON_IRANIAN
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OTPReceiver.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OTPReceiver.kt
new file mode 100644
index 000000000..c54e22758
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OTPReceiver.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class OTPReceiver(
+ val receiver: String,
+ val type: OTPType,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OTPType.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OTPType.kt
new file mode 100644
index 000000000..45d19c8e3
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OTPType.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+enum class OTPType {
+
+ SMS, EMAIL,
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderAdminItem.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderAdminItem.kt
new file mode 100644
index 000000000..aec8f5592
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderAdminItem.kt
@@ -0,0 +1,24 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+
+data class OrderAdminItem(
+ val symbol: String,
+ val ouid: String,
+ val orderType: MatchingOrderType?,
+ val side: OrderDirection,
+ val price: BigDecimal,
+ val quantity: BigDecimal,
+ val quoteQuantity: BigDecimal?,
+ val executedQuantity: BigDecimal?,
+ val takerFee: BigDecimal?,
+ val makerFee: BigDecimal?,
+ val status: OrderStatus?,
+ val appearance: Int?,
+ val createDate: LocalDateTime,
+ val updateDate: LocalDateTime,
+ val uuid: String?,
+ val ownerName: String? = null,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt
new file mode 100644
index 000000000..d10688d1b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt
@@ -0,0 +1,22 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+import java.util.*
+
+data class OrderData(
+ val symbol: String,
+ val orderId: Long,
+ val orderType: MatchingOrderType,
+ val side: OrderDirection,
+ val price: BigDecimal,
+ val quantity: BigDecimal,
+ val quoteQuantity: BigDecimal,
+ val executedQuantity: BigDecimal,
+ val takerFee: BigDecimal,
+ val makerFee: BigDecimal,
+ val status: OrderStatus,
+ val appearance: Int,
+ val createDate: LocalDateTime,
+ val updateDate: LocalDateTime,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt
index aba4d3351..bac8c0752 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt
@@ -6,16 +6,42 @@ enum class TimeInForce {
FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution.
}
-enum class OrderStatus(val code: Int) {
+enum class OrderStatus(val code: Int, val orderOfAppearance: Int) {
- REQUESTED(0),
- 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.
- 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)
+ REQUESTED(0, 0),
+ NEW(1, 1), //The order has been accepted by the engine.
+ PARTIALLY_FILLED(4, 2), //A part of the order has been filled.
+ FILLED(5, 3), //The order has been completed.
+ CANCELED(2, 3), //The order has been canceled by the user.
+ REJECTED(3, 3), //The order was not accepted by the engine and not processed.
+ EXPIRED(
+ 6,
+ 3
+ ); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance)
+ fun comesBefore(status: OrderStatus?): Boolean {
+ if (status == null)
+ return false
+ return orderOfAppearance < status.orderOfAppearance
+ }
+
+ fun comesAfter(status: OrderStatus?): Boolean {
+ if (status == null)
+ return false
+ return orderOfAppearance > status.orderOfAppearance
+ }
+
+ fun isOpenOrder(): Boolean {
+ return this == NEW || this == PARTIALLY_FILLED
+ }
+
+ companion object {
+ fun fromCode(code: Int?): OrderStatus? {
+ if (code == null)
+ return null
+ return values().find { it.code == code }
+ }
+ }
fun isWorking(): Boolean {
return listOf(NEW, PARTIALLY_FILLED).contains(this)
}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfig.kt
new file mode 100644
index 000000000..d1741a047
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfig.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+class PairConfig(
+ val pair: String,
+ val leftSideWalletSymbol: String,
+ val rightSideWalletSymbol: String,
+ val leftSideFraction: BigDecimal,
+ val rightSideFraction: BigDecimal
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfigResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfigResponse.kt
new file mode 100644
index 000000000..da44479aa
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairConfigResponse.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class PairConfigResponse(
+ val pair: String,
+ val leftSideWalletSymbol: String,
+ val rightSideWalletSymbol: String,
+ val leftSideFraction: BigDecimal,
+ val rightSideFraction: BigDecimal
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt
index f7e3fd12c..d6c00fe0e 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairInfoResponse.kt
@@ -4,8 +4,10 @@ import java.math.BigDecimal
data class PairInfoResponse(
val pair: String,
- val leftSideWalletSymbol: String,
- val rightSideWalletSymbol: String,
- val leftSideFraction: BigDecimal,
- val rightSideFraction: BigDecimal
+ val baseAsset: String,
+ val quoteAsset: String,
+ val isAvailable: Boolean,
+ val minOrder : BigDecimal,
+ val maxOrder : BigDecimal,
+ val orderTypes : String,
)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairSetting.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairSetting.kt
new file mode 100644
index 000000000..b3d7d5946
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairSetting.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+class PairSetting(
+ val pair: String,
+ val isAvailable: Boolean,
+ val minOrder : BigDecimal,
+ val maxOrder : BigDecimal,
+ val orderTypes : String,
+ val updateDate: LocalDateTime? = null,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Profile.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Profile.kt
new file mode 100644
index 000000000..7141dc707
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Profile.kt
@@ -0,0 +1,26 @@
+package co.nilin.opex.api.core.inout
+
+import java.time.LocalDateTime
+
+data class Profile(
+ var email: String?,
+ var userId: String?,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var mobile: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: NationalityType? = null,
+ var identifier: String? = null,
+ var gender: Gender? = null,
+ var birthDate: LocalDateTime? = null,
+ var status: ProfileStatus? = null,
+ var createDate: LocalDateTime? = null,
+ var lastUpdateDate: LocalDateTime? = null,
+ var creator: String? = null,
+ var kycLevel: KycLevel? = null,
+ var mobileIdentityMatch: Boolean? = null,
+ var personalIdentityMatch: Boolean? = null
+
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalAdminResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalAdminResponse.kt
new file mode 100644
index 000000000..9c1700143
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalAdminResponse.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+import java.time.LocalDateTime
+
+data class ProfileApprovalAdminResponse(
+ var id: Long,
+ var userId: String,
+ var status: ProfileApprovalRequestStatus,
+ var createDate: LocalDateTime,
+ var updateDate: LocalDateTime? = null,
+ var updater: String? = null,
+ var description: String? = null,
+ var firstName: String? = null,
+ var lastName: String? = null
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestFilter.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestFilter.kt
new file mode 100644
index 000000000..1734ac018
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestFilter.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.core.inout
+
+data class ProfileApprovalRequestFilter(
+ val userId: String?,
+ val status: ProfileApprovalRequestStatus?,
+ val createDateFrom: Long?,
+ val createDateTo: Long?,
+ val limit: Int = 10,
+ val offset: Int = 0,
+ val ascendingByTime: Boolean = false,
+
+ )
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestStatus.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestStatus.kt
new file mode 100644
index 000000000..cdf56aaac
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestStatus.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+enum class ProfileApprovalRequestStatus {
+ PENDING, APPROVED, REJECTED
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestUser.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestUser.kt
new file mode 100644
index 000000000..1cc8b19f1
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestUser.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.api.core.inout
+
+import java.time.LocalDateTime
+
+data class ProfileApprovalRequestUser(
+ var status: ProfileApprovalRequestStatus,
+ var createDate: LocalDateTime,
+ var description: String? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestUserResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestUserResponse.kt
new file mode 100644
index 000000000..48af7b292
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileApprovalRequestUserResponse.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.core.inout
+
+data class ProfileApprovalRequestUserResponse(
+ var status: ProfileApprovalRequestStatus,
+ var createDate: Long,
+ var description: String? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileHistory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileHistory.kt
new file mode 100644
index 000000000..255e7f517
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileHistory.kt
@@ -0,0 +1,29 @@
+package co.nilin.opex.api.core.inout
+
+import java.time.LocalDateTime
+
+data class ProfileHistory(
+ var email: String?,
+ var userId: String?,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var mobile: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: String? = null,
+ var identifier: String? = null,
+ var gender: Boolean? = null,
+ var birthDate: LocalDateTime? = null,
+ var status: ProfileStatus? = null,
+ var createDate: LocalDateTime? = null,
+ var lastUpdateDate: LocalDateTime? = null,
+ var creator: String? = null,
+ var changeRequestDate: LocalDateTime?,
+ var changeRequestType: String?,
+ var updatedItem: List?,
+ var kycLevel: KycLevel? = null,
+ var mobileIdentityMatch: Boolean? = null,
+ var personalIdentityMatch: Boolean? = null
+
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileRequest.kt
new file mode 100644
index 000000000..42be3f18e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileRequest.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.api.core.inout
+
+data class ProfileRequest(
+ var userId: String? = null,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var mobile: String? = null,
+ var email: String? = null,
+ var identifier: String? = null,
+ var nationality: NationalityType? = null,
+ var gender: Gender? = null,
+ var status: ProfileStatus? = null,
+ var kycLevel: KycLevel? = null,
+ var createDateFrom: Long? = null,
+ var createDateTo: Long? = null,
+ var limit : Int = 10,
+ var offset: Int = 0,
+ var ascendingByTime: Boolean = false,)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileResponse.kt
new file mode 100644
index 000000000..41d4eab88
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileResponse.kt
@@ -0,0 +1,24 @@
+package co.nilin.opex.api.core.inout
+
+data class ProfileResponse(
+ var email: String?,
+ var userId: String?,
+ var firstName: String? = null,
+ var lastName: String? = null,
+ var address: String? = null,
+ var mobile: String? = null,
+ var telephone: String? = null,
+ var postalCode: String? = null,
+ var nationality: NationalityType? = null,
+ var identifier: String? = null,
+ var gender: Gender? = null,
+ var birthDate: Long? = null,
+ var status: ProfileStatus? = null,
+ var createDate: Long? = null,
+ var lastUpdateDate: Long? = null,
+ var creator: String? = null,
+ var kycLevel: KycLevel? = null,
+ var mobileIdentityMatch: Boolean? = null,
+ var personalIdentityMatch: Boolean? = null
+
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileStatus.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileStatus.kt
new file mode 100644
index 000000000..9e7bb6113
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ProfileStatus.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+enum class ProfileStatus {
+ CREATED,
+ CONTACT_INFO_COMPLETED,
+ PROFILE_COMPLETED,
+ SYSTEM_APPROVED,
+ PENDING_ADMIN_APPROVAL,
+ ADMIN_REJECTED,
+ ADMIN_APPROVED
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/QuoteCurrency.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/QuoteCurrency.kt
new file mode 100644
index 000000000..a306d22c5
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/QuoteCurrency.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+import java.time.LocalDateTime
+
+data class QuoteCurrency(
+ val currency: String,
+ val isReference: Boolean = false,
+ var lastUpdateDate: LocalDateTime = LocalDateTime.now(),
+ val displayOrder: Int ? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitEndpoint.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitEndpoint.kt
new file mode 100644
index 000000000..928a6c805
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitEndpoint.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+data class RateLimitEndpoint(
+ val id: Long? = null,
+ val url: String,
+ val method: String,
+ val groupId: Long,
+ val priority: Int,
+ val enabled: Boolean = true
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitGroup.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitGroup.kt
new file mode 100644
index 000000000..e9e3db23e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitGroup.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+data class RateLimitGroup(
+ val id: Long? = null,
+ val name: String,
+ val requestCount: Int,
+ val requestWindowSeconds: Int,
+ val cooldownSeconds: Int,
+ val maxPenaltyLevel: Int,
+ val enabled: Boolean = true
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitPenalty.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitPenalty.kt
new file mode 100644
index 000000000..fb078266b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RateLimitPenalty.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+data class RateLimitPenalty(
+ val id: Long? = null,
+ val groupId: Long,
+ val blockStep: Int,
+ val blockDurationSeconds: Int
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestDepositBody.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestDepositBody.kt
new file mode 100644
index 000000000..3168eb16b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestDepositBody.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class RequestDepositBody(
+ val symbol: String,
+ val receiverUuid: String,
+ val receiverWalletType: WalletType,
+ val amount: BigDecimal,
+ val description: String?,
+ val transferRef: String?,
+ val gatewayUuid: String?,
+ val chain: String?,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestWithdrawBody.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestWithdrawBody.kt
new file mode 100644
index 000000000..766c74c42
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/RequestWithdrawBody.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class RequestWithdrawBody(
+ val currency: String,
+ val amount: BigDecimal,
+ val destSymbol: String?,
+ val destAddress: String,
+ val destNetwork: String?,
+ val destNote: String?,
+ val gatewayUuid: String?
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ReservedTransferResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ReservedTransferResponse.kt
new file mode 100644
index 000000000..31ad0bbec
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ReservedTransferResponse.kt
@@ -0,0 +1,20 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class ReservedTransferResponse(
+ var reserveNumber: String,
+ var sourceSymbol: String,
+ var destSymbol: String,
+ var receiverUuid: String,
+ var sourceAmount: BigDecimal,
+ var guaranteedDestAmount: BigDecimal,
+ var reserveDate: LocalDateTime? = LocalDateTime.now(),
+ var expDate: LocalDateTime? = null,
+ var status: ReservedStatus? = null
+)
+
+
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ResolveUsersRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ResolveUsersRequest.kt
new file mode 100644
index 000000000..66d781fcc
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/ResolveUsersRequest.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+data class ResolveUsersRequest(
+ val uuids: List
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SubmitVoucherResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SubmitVoucherResponse.kt
new file mode 100644
index 000000000..73f2ba3c8
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SubmitVoucherResponse.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class SubmitVoucherResponse(
+ val amount: BigDecimal,
+ val currency: String,
+ var issuer: String?,
+ var description: String? = null
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapAdminResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapAdminResponse.kt
new file mode 100644
index 000000000..2bddf2b89
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapAdminResponse.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class SwapAdminResponse(
+ var reserveNumber: String,
+ var sourceSymbol: String,
+ var destSymbol: String,
+ var uuid: String,
+ var ownerName: String?,
+ var sourceAmount: BigDecimal,
+ var reservedDestAmount: BigDecimal,
+ var reserveDate: LocalDateTime? = LocalDateTime.now(),
+ var expDate: LocalDateTime? = null,
+ var status: ReservedStatus? = null,
+ val rate: BigDecimal? = null,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt
new file mode 100644
index 000000000..efea1245f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class SwapResponse(
+ var reserveNumber: String,
+ var sourceSymbol: String,
+ var destSymbol: String,
+ var uuid: String,
+ var sourceAmount: BigDecimal,
+ var reservedDestAmount: BigDecimal,
+ var reserveDate: LocalDateTime? = LocalDateTime.now(),
+ var expDate: LocalDateTime? = null,
+ var status: ReservedStatus? = null,
+ val rate: BigDecimal? = null,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TempOtpResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TempOtpResponse.kt
new file mode 100644
index 000000000..15ed86cf9
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TempOtpResponse.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.api.core.inout
+
+data class TempOtpResponse(val otp: String?, var receivers: List? = null)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt
new file mode 100644
index 000000000..5439e9feb
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalCommand.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+data class TerminalCommand(
+ var uuid: String?,
+ var owner: String? = null,
+ var identifier: String,
+ var active: Boolean? = true,
+ var metaData: String,
+ var description: String? = null,
+ var displayOrder: Int? = null,
+ )
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalLocalizationCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalLocalizationCommand.kt
new file mode 100644
index 000000000..eaba72509
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalLocalizationCommand.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+data class TerminalLocalizationCommand(
+ var id : Long?,
+ var description: String?=null,
+ var owner: String?=null,
+ var language: String,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalLocalizationResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalLocalizationResponse.kt
new file mode 100644
index 000000000..8bcd6f71e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalLocalizationResponse.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class TerminalLocalizationResponse(
+ val terminalUuid: String,
+ val terminalLocalizations: List
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalUpdateCommand.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalUpdateCommand.kt
new file mode 100644
index 000000000..c77b1a668
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TerminalUpdateCommand.kt
@@ -0,0 +1,9 @@
+package co.nilin.opex.api.core.inout
+
+data class TerminalUpdateCommand(
+ var uuid: String?,
+ var identifier: String,
+ var active: Boolean? = true,
+ var metaData: String,
+ var displayOrder: Int? = null,
+ )
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt
index 6212380ba..31ca765d9 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/Trade.kt
@@ -1,6 +1,7 @@
package co.nilin.opex.api.core.inout
import java.math.BigDecimal
+import java.time.LocalDateTime
import java.util.*
data class Trade(
@@ -12,7 +13,7 @@ data class Trade(
val quoteQuantity: BigDecimal,
val commission: BigDecimal,
val commissionAsset: String,
- val time: Date,
+ val time: LocalDateTime,
val isBuyer: Boolean,
val isMaker: Boolean,
val isBestMatch: Boolean,
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TradeAdminItem.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TradeAdminItem.kt
new file mode 100644
index 000000000..94638023c
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TradeAdminItem.kt
@@ -0,0 +1,30 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+
+data class TradeAdminItem(
+ val tradeId: Long,
+ val symbol: String,
+ val baseAsset: String?,
+ val quoteAsset: String?,
+ val matchedPrice: BigDecimal,
+ val matchedQuantity: BigDecimal,
+ val takerPrice: BigDecimal,
+ val makerPrice: BigDecimal,
+ val tradeDate: LocalDateTime,
+ val makerUuid: String,
+ val takerUuid: String,
+ val makerOuid: String?,
+ val takerOuid: String?,
+ val makerCommission: BigDecimal?,
+ val takerCommission: BigDecimal?,
+ val makerCommissionAsset: String?,
+ val takerCommissionAsset: String?,
+ // Enriched in API when includeNames=true
+ val makerOwnerName: String? = null,
+ val takerOwnerName: String? = null,
+)
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TradeAdminResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TradeAdminResponse.kt
new file mode 100644
index 000000000..9164db6f5
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TradeAdminResponse.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+@Deprecated("Use TradeResponseItem")
+data class TradeAdminResponse(
+ val id: Long,
+ val currency: String,
+ val sourceOwnerUuid: String,
+ val sourceOwnerName: String?,
+ val destOwnerUuid: String,
+ val destOwnerName: String?,
+ val amount: BigDecimal,
+ val description: String?,
+ val ref: String?,
+ val date: LocalDateTime,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionHistoryResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionHistoryResponse.kt
deleted file mode 100644
index 5b03de954..000000000
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionHistoryResponse.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package co.nilin.opex.api.core.inout
-
-import java.math.BigDecimal
-
-data class TransactionHistoryResponse(
- val id: Long,
- val currency: String,
- val amount: BigDecimal,
- val description: String?,
- val ref: String?,
- val date: Long
-)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionSummary.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionSummary.kt
new file mode 100644
index 000000000..d0494d022
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransactionSummary.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class TransactionSummary(
+ val currency: String,
+ val amount: BigDecimal,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt
new file mode 100644
index 000000000..838b2ba40
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferCategory.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.api.core.inout
+
+enum class TransferCategory {
+
+ NO_CATEGORY,
+ DEPOSIT,
+ DEPOSIT_MANUALLY,
+ WITHDRAW_REQUEST,
+ WITHDRAW_ACCEPT,
+ WITHDRAW_REJECT,
+ WITHDRAW_CANCEL,
+ PURCHASE_FINALIZED,
+ IMPERSONATED_PURCHASE_FINALIZED,
+ WITHDRAW_MANUALLY,
+ ORDER_CREATE,
+ ORDER_CANCEL,
+ ORDER_FINALIZED,
+ TRADE,
+ FEE,
+ REFERRAL_COMMISSION,
+ REFERRAL_KYC_REWARD,
+ REFERENT_COMMISSION,
+ KYC_ACCEPTED_REWARD,
+ NORMAL //TODO TEST?
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferMethod.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferMethod.kt
new file mode 100644
index 000000000..3cfd6a8a3
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferMethod.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout
+
+enum class TransferMethod {
+ CARD, SHEBA, IPG, EXCHANGE , MANUALLY , VOUCHER, MPG , REWARD, BANK_DEPOSIT, ACCOUNT
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferReserveRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferReserveRequest.kt
new file mode 100644
index 000000000..3fb3262e3
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferReserveRequest.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class TransferReserveRequest(
+ val sourceAmount: BigDecimal,
+ val sourceSymbol: String,
+ val destSymbol: String,
+ var senderUuid: String?,
+ val receiverUuid: String,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferResult.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferResult.kt
new file mode 100644
index 000000000..f6a3657b6
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/TransferResult.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+data class TransferResult(
+ val date: Long,
+ val sourceUuid: String,
+ val sourceWalletType: WalletType,
+ val sourceBalanceBeforeAction: Amount,
+ val sourceBalanceAfterAction: Amount,
+ val amount: Amount,
+ val destUuid: String,
+ val destWalletType: WalletType,
+ val receivedAmount: Amount,
+ val sourceWallet: Long? = null,
+ val destWallet: Long? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateApprovalRequestBody.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateApprovalRequestBody.kt
new file mode 100644
index 000000000..df0857325
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateApprovalRequestBody.kt
@@ -0,0 +1,7 @@
+package co.nilin.opex.api.core.inout
+
+data class UpdateApprovalRequestBody(
+ val id: Long,
+ val description: String?,
+ val status: ProfileApprovalRequestStatus
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateUserConfigRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateUserConfigRequest.kt
new file mode 100644
index 000000000..a0f57f949
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateUserConfigRequest.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import co.nilin.opex.common.data.CalenderType
+import co.nilin.opex.common.data.Theme
+import co.nilin.opex.common.data.UserLanguage
+
+data class UpdateUserConfigRequest(
+ val theme: Theme?,
+ val language: UserLanguage?,
+ val calender: CalenderType?
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateWebConfigRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateWebConfigRequest.kt
new file mode 100644
index 000000000..c855a89b2
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UpdateWebConfigRequest.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.api.core.inout
+
+import co.nilin.opex.common.data.CalenderType
+import co.nilin.opex.common.data.LanguageOption
+import co.nilin.opex.common.data.Theme
+import co.nilin.opex.common.data.UserLanguage
+
+data class UpdateWebConfigRequest(
+ val logoUrl: String?,
+ val title: String?,
+ val description: String?,
+ var defaultLanguage: UserLanguage?,
+ var supportedLanguages: Map?,
+ var supportedCalenders: List?,
+ val defaultTheme: Theme?,
+ val supportedThemes: List?,
+ val supportEmail: String?,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserDetailAssetsSnapshot.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserDetailAssetsSnapshot.kt
new file mode 100644
index 000000000..1b66fac00
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserDetailAssetsSnapshot.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class UserDetailAssetsSnapshot(
+ val uuid: String,
+ val currencySnapshots: List,
+ val totalAmount: BigDecimal,
+ val quoteCurrency: String,
+ val snapshotDate: LocalDateTime,
+)
+
+data class CurrencyAssetsSnapshot(
+ val currency: String,
+ val volume: BigDecimal,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairFeeResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt
similarity index 54%
rename from api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairFeeResponse.kt
rename to api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt
index 580ff3358..83f15dbd4 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/PairFeeResponse.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt
@@ -2,10 +2,10 @@ package co.nilin.opex.api.core.inout
import java.math.BigDecimal
-data class PairFeeResponse(
- val pair: String,
- val direction: String,
- val userLevel: String,
+data class UserFee(
+ val name: String,
val makerFee: BigDecimal,
val takerFee: BigDecimal
-)
\ No newline at end of file
+ )
+
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserLevelConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserLevelConfig.kt
new file mode 100644
index 000000000..b6e05b8af
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserLevelConfig.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.api.core.inout
+
+data class UserLevelConfig(
+ val userLevel: String,
+ val language: String,
+ val name: String,
+ val description: String? = null,
+ val permissions: Permissions
+)
+
+data class Permissions(
+ val onChainDepositAllowed: Boolean? = false,
+ val offChainDepositAllowed: Boolean? = false,
+ val onChainWithdrawAllowed: Boolean? = false,
+ val offChainWithdrawAllowed: Boolean? = false
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserSwapTransactionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserSwapTransactionRequest.kt
new file mode 100644
index 000000000..02504e5e0
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserSwapTransactionRequest.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.core.inout
+
+data class UserSwapTransactionRequest(
+ val userId: String? = null,
+ val sourceSymbol: String?,
+ val destSymbol: String?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+ val ascendingByTime: Boolean = false,
+ val status: ReservedStatus? = ReservedStatus.Committed
+)
+
+enum class ReservedStatus {
+ Created, Expired, Committed,
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt
new file mode 100644
index 000000000..1e12ff483
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionCategory.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.api.core.inout
+
+enum class UserTransactionCategory {
+
+ TRADE,
+ DEPOSIT,
+ DEPOSIT_TO, // for admin using DEPOSIT_MANUALLY
+ WITHDRAW_FROM, // for admin using DEPOSIT_MANUALLY
+ WITHDRAW,
+ FEE,
+ SWAP,
+ IMPERSONATED_SWAP,
+ REFERRAL_COMMISSION,
+ REFERRAL_KYC_REWARD,
+ REFERENT_COMMISSION,
+ KYC_ACCEPTED_REWARD,
+ SYSTEM
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionHistory.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionHistory.kt
new file mode 100644
index 000000000..c86873a2f
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionHistory.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+data class UserTransactionHistory(
+ val id: String,
+ val userId: String,
+ val ownerName: String?,
+ val currency: String,
+ val balance: BigDecimal,
+ val balanceChange: BigDecimal,
+ val category: UserTransactionCategory,
+ val description: String?,
+ val date: LocalDateTime
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt
new file mode 100644
index 000000000..898d4c420
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.core.inout
+
+data class UserTransactionRequest(
+ val userId: String? = null,
+ val currency: String?,
+ val sourceSymbol: String?,
+ val destSymbol: String?,
+ val category: UserTransactionCategory?,
+ val startTime: Long? = null,
+ val endTime: Long? = null,
+ val limit: Int? = 10,
+ val offset: Int? = 0,
+ val ascendingByTime: Boolean = false,
+)
+
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserWebConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserWebConfig.kt
new file mode 100644
index 000000000..c30dc0afe
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserWebConfig.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+import co.nilin.opex.common.data.CalenderType
+import co.nilin.opex.common.data.Theme
+import co.nilin.opex.common.data.UserLanguage
+
+data class UserWebConfig(
+ var theme: Theme,
+ var language: UserLanguage,
+ var calender: CalenderType? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletDataResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletDataResponse.kt
new file mode 100644
index 000000000..091184fdd
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletDataResponse.kt
@@ -0,0 +1,14 @@
+package co.nilin.opex.api.core.inout
+
+data class WalletDataResponse(
+ val uuid: String,
+ val title: String,
+ val wallets: List
+)
+
+data class WalletCurrencyData(
+ val currency: String = "",
+ val free: Double = 0.0,
+ val locked: Double = 0.0,
+ val pendingWithdraw: Double = 0.0
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletTotal.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletTotal.kt
new file mode 100644
index 000000000..308dd7391
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletTotal.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+data class WalletTotal(
+ val currency: String,
+ val balance: Double
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletType.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletType.kt
new file mode 100644
index 000000000..adc7a6b4c
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WalletType.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout
+
+enum class WalletType {
+
+ MAIN, EXCHANGE, CASHOUT
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawActionResult.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawActionResult.kt
new file mode 100644
index 000000000..e51cbafb7
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawActionResult.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.core.inout
+
+class WithdrawActionResult(
+ val withdrawId: String, val status: WithdrawStatus, val nextAction: WithdrawNextAction? = null
+)
+
+enum class WithdrawNextAction() {
+ OTP_EMAIL,
+ OTP_MOBILE,
+ WAITING_FOR_ADMIN;
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawAdminResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawAdminResponse.kt
new file mode 100644
index 000000000..451db51e2
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawAdminResponse.kt
@@ -0,0 +1,28 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+class WithdrawAdminResponse(
+ val withdrawId: String,
+ val uuid: String,
+ val ownerName: String?,
+ val amount: BigDecimal,
+ val currency: String,
+ val appliedFee: BigDecimal,
+ val destAmount: BigDecimal?,
+ val destSymbol: String?,
+ val destAddress: String?,
+ val destNetwork: String?,
+ var destNote: String?,
+ var destTransactionRef: String?,
+ val statusReason: String?,
+ val status: WithdrawStatus,
+ var applicator: String?,
+ var withdrawType: WithdrawType,
+ var attachment: String?,
+ val createDate: LocalDateTime,
+ val lastUpdateDate: LocalDateTime?,
+ var transferMethod: TransferMethod?,
+ val otpRequired: Int?,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawDoneRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawDoneRequest.kt
new file mode 100644
index 000000000..8990f55bb
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawDoneRequest.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class WithdrawDoneRequest(
+ val destTransactionRef: String,
+ val destNote: String?,
+ val destAmount: BigDecimal?,
+ val attachment: String?
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt
deleted file mode 100644
index 3d0d6607a..000000000
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawHistoryResponse.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package co.nilin.opex.api.core.inout
-
-import java.math.BigDecimal
-
-data class WithdrawHistoryResponse(
- val withdrawId: Long?,
- val uuid: String,
- val amount: BigDecimal,
- val currency: String,
- val acceptedFee: BigDecimal,
- val appliedFee: BigDecimal?,
- val destAmount: BigDecimal?,
- val destSymbol: String?,
- val destAddress: String?,
- val destNetwork: String?,
- var destNote: String?,
- var destTransactionRef: String?,
- val statusReason: String?,
- val status: String,
- val createDate: Long,
- val acceptDate: Long?
-)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawLimitConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawLimitConfig.kt
new file mode 100644
index 000000000..9e15e3316
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawLimitConfig.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+
+data class WithdrawLimitConfig(
+ val name: String,
+ val dailyMaxAmount: BigDecimal,
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawRejectRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawRejectRequest.kt
new file mode 100644
index 000000000..b15b72688
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawRejectRequest.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.api.core.inout
+
+data class WithdrawRejectRequest(val reason: String, val attachment: String?)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt
new file mode 100644
index 000000000..9e9634efa
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawResponse.kt
@@ -0,0 +1,30 @@
+package co.nilin.opex.api.core.inout
+
+import java.math.BigDecimal
+import java.time.LocalDateTime
+
+class WithdrawResponse(
+ val withdrawId: String,
+ val uuid: String,
+ val amount: BigDecimal,
+ val currency: String,
+ val appliedFee: BigDecimal,
+ val destAmount: BigDecimal?,
+ val destSymbol: String?,
+ val destAddress: String?,
+ val destNetwork: String?,
+ var destNote: String?,
+ var destTransactionRef: String?,
+ val statusReason: String?,
+ val status: WithdrawStatus,
+ var applicator: String?,
+ var withdrawType: WithdrawType,
+ var attachment: String?,
+ val createDate: LocalDateTime,
+ val lastUpdateDate: LocalDateTime?,
+ var transferMethod: TransferMethod?,
+ val otpRequired: Int? = 0,
+)
+enum class WithdrawType {
+ ON_CHAIN, OFF_CHAIN
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawStatus.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawStatus.kt
new file mode 100644
index 000000000..277f82de7
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/WithdrawStatus.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout
+
+enum class WithdrawStatus {
+ REQUESTED,
+ CREATED,
+ ACCEPTED,
+ CANCELED,
+ REJECTED,
+ DONE
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt
new file mode 100644
index 000000000..11382715b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.core.inout.analytics
+
+import java.math.BigDecimal
+
+
+data class ActivityTotals(
+ val totalBalance: BigDecimal,
+ val totalWithdraw: BigDecimal,
+ val totalDeposit: BigDecimal,
+ val totalTrade: BigDecimal,
+ val totalOrder: BigDecimal
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/DailyAmount.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/DailyAmount.kt
new file mode 100644
index 000000000..f07212c51
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/DailyAmount.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.inout.analytics
+
+import java.math.BigDecimal
+import java.time.LocalDate
+
+data class DailyAmount(val date: LocalDate, var totalAmount: BigDecimal)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Attribute.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Attribute.kt
new file mode 100644
index 000000000..f04ab4739
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Attribute.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout.auth
+
+data class Attribute(
+ val key: String,
+ val value: String
+)
+
+object Attributes {
+
+ const val EMAIL = "email"
+ const val MOBILE = "mobile"
+ const val OTP = "otpConfig"
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Captcha.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Captcha.kt
new file mode 100644
index 000000000..c428bcb6e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Captcha.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout.auth
+
+enum class CaptchaType {
+ INTERNAL, ARCAPTCHA, HCAPTCHA
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Device.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Device.kt
new file mode 100644
index 000000000..7162681b9
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Device.kt
@@ -0,0 +1,8 @@
+package co.nilin.opex.api.core.inout.auth
+
+open class Device {
+ var deviceUuid: String?=null
+ var appVersion: String?=null
+ var osVersion: String?=null
+ var os: Os?=null
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/OTP.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/OTP.kt
new file mode 100644
index 000000000..91d46420e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/OTP.kt
@@ -0,0 +1,33 @@
+package co.nilin.opex.api.core.inout.auth
+
+import co.nilin.opex.api.core.inout.OTPType
+
+data class OTPReceiver(
+ val receiver: String,
+ val type: OTPType,
+)
+
+data class OTPCode(
+ val code: String,
+ val otpType: OTPType,
+)
+
+data class OTPVerifyRequest(
+ val userId: String,
+ val otpCodes: List
+)
+
+data class OTPVerifyResponse(
+ val result: Boolean,
+ val type: OTPResultType
+)
+
+data class TempOtpResponse(val otp: String?, val otpReceiver: OTPReceiver?)
+
+enum class OTPAction {
+ REGISTER, FORGET, NONE
+}
+
+enum class OTPResultType {
+ VALID, EXPIRED, INCORRECT, INVALID
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Session.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Session.kt
new file mode 100644
index 000000000..19b255363
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Session.kt
@@ -0,0 +1,27 @@
+package co.nilin.opex.api.core.inout.auth
+
+import java.time.LocalDateTime
+
+data class Sessions(
+ val deviceUuid: String?,
+ val os: Os?,
+ val osVersion: String?,
+ val appVersion: String?,
+ val firstLoginDate: LocalDateTime?,
+ val lastLoginDate: LocalDateTime?,
+ val sessionState: String?,
+ val sessionStatus: SessionStatus?,
+ val sessionCreateDate: LocalDateTime?,
+ val sessionExpireDate: LocalDateTime?,
+ var isCurrentSession: Boolean?=false
+)
+
+enum class SessionStatus {
+ ACTIVE,
+ EXPIRED,
+ TERMINATED
+}
+
+enum class Os {
+ ANDROID, IOS, MOBILE_WEB, DESKTOP_WEB
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/SessionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/SessionRequest.kt
new file mode 100644
index 000000000..85a001484
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/SessionRequest.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout.auth
+
+data class SessionRequest(
+ var uuid: String? = null,
+ val limit: Int = 10,
+ val offset: Int = 0,
+ val ascendingByTime: Boolean = false,
+ val os: Os? = null,
+ val status: SessionStatus? = null
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Token.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Token.kt
new file mode 100644
index 000000000..6a346f78a
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/Token.kt
@@ -0,0 +1,88 @@
+package co.nilin.opex.api.core.inout.auth
+
+import co.nilin.opex.api.core.inout.OTPType
+import co.nilin.opex.api.core.inout.auth.CaptchaType
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class PasswordFlowTokenRequest(
+ val username: String,
+ val password: String,
+ val clientId: String,
+ val clientSecret: String?,
+ val rememberMe: Boolean = true,
+ val captchaType: CaptchaType? = CaptchaType.INTERNAL,
+ val captchaCode: String,
+):Device()
+
+data class ConfirmPasswordFlowTokenRequest(
+ val username: String,
+ val token: String,
+ val clientId: String,
+ val clientSecret: String?,
+ val otp: String,
+ val rememberMe: Boolean = true,
+): Device()
+
+data class ResendOtpRequest(
+ val username: String,
+ val clientId: String
+)
+
+
+data class RefreshTokenRequest(
+ val clientId: String,
+ val clientSecret: String?,
+ val refreshToken: String
+):Device()
+
+data class ExternalIdpTokenRequest(
+ val idToken: String,
+ val accessToken: String,
+ val idp: String,
+ val otpVerifyRequest: OTPVerifyRequest?
+):Device()
+
+data class Token(
+ @JsonProperty("access_token")
+ val accessToken: String, // The access token
+
+ @JsonProperty("expires_in")
+ val expiresIn: Int, // Expiration time of the access token in seconds
+
+ @JsonProperty("refresh_expires_in")
+ var refreshExpiresIn: Int?, // Expiration time of the refresh token in seconds
+
+ @JsonProperty("refresh_token")
+ var refreshToken: String?, // The refresh token
+
+ @JsonProperty("token_type")
+ val tokenType: String?, // Type of token (usually "Bearer")
+
+ @JsonProperty("not-before-policy")
+ val notBeforePolicy: Int?, // Timestamp indicating when the token becomes valid
+
+ @JsonProperty("session_state")
+ val sessionState: String?, // Session state (optional)
+
+ @JsonProperty("scope")
+ val scope: String? // Scopes associated with the token
+
+)
+
+data class TokenResponse(
+ val token: Token?,
+ val otp: RequiredOTP?,
+ //TODO IMPORTANT: remove in production
+ val otpCode: String?,
+)
+
+data class RequiredOTP(
+ val type: OTPType,
+ val receiver: String?
+)
+
+data class ResendOtpResponse(
+ val otp: RequiredOTP?,
+ //TODO IMPORTANT: remove in production
+ val otpCode: String?,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/UserRegister.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/UserRegister.kt
new file mode 100644
index 000000000..917fcd1fd
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/auth/UserRegister.kt
@@ -0,0 +1,66 @@
+package co.nilin.opex.api.core.inout.auth
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+data class RegisterUserRequest(
+ val username: String,
+ val firstName: String? = null,
+ val lastName: String? = null,
+ val captchaType: CaptchaType? = CaptchaType.INTERNAL,
+ val captchaCode: String,
+)
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class VerifyOTPRequest(
+ val username: String,
+ val otp: String,
+)
+
+data class OTPActionTokenResponse(
+ val token: String,
+)
+
+data class ConfirmRegisterRequest(
+ val password: String,
+ val token: String,
+ val clientId: String?,
+ val clientSecret: String?,
+) : Device()
+
+data class TokenData(
+ val isValid: Boolean,
+ val userId: String,
+ val action: OTPAction,
+)
+
+data class ExternalIdpUserRegisterRequest(
+ val idToken: String,
+ val idp: String,
+ val password: String,
+ val otpVerifyRequest: OTPVerifyRequest?,
+)
+
+data class KeycloakUser(
+ val id: String,
+ val username: String,
+ val email: String?,
+ val firstName: String?,
+ val lastName: String?,
+ val emailVerified: Boolean,
+ val enabled: Boolean,
+ val attributes: Map>?,
+) {
+ val mobile: String? = attributes?.get(Attributes.MOBILE)?.get(0)
+}
+
+data class ConfirmForgetRequest(
+ val newPassword: String,
+ val newPasswordConfirmation: String,
+ val token: String,
+)
+
+data class ForgotPasswordRequest(
+ val username: String,
+ val captchaType: CaptchaType? = CaptchaType.INTERNAL,
+ val captchaCode: String,
+)
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyExchangeRatesResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyExchangeRatesResponse.kt
new file mode 100644
index 000000000..f21c1760c
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyExchangeRatesResponse.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout.otc
+
+import java.math.BigDecimal
+
+data class CurrencyExchangeRate(
+ val sourceSymbol: String,
+ val destSymbol: String,
+ val rate: BigDecimal,
+ val isSwappable: Boolean
+
+)
+
+data class CurrencyExchangeRatesResponse(val rates: List)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPair.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPair.kt
new file mode 100644
index 000000000..bb2ec06bc
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPair.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout.otc
+
+import co.nilin.opex.common.OpexError
+
+data class CurrencyPair(
+ val sourceSymbol: String,
+ val destSymbol: String
+) {
+ fun validate() {
+ if (sourceSymbol == destSymbol)
+ throw OpexError.SourceIsEqualDest.exception()
+ }
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPrice.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPrice.kt
new file mode 100644
index 000000000..ac6536b5e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPrice.kt
@@ -0,0 +1,5 @@
+package co.nilin.opex.api.core.inout.otc
+
+import java.math.BigDecimal
+
+data class CurrencyPrice(var currency: String, val buyPrice: BigDecimal? = null, var sellPrice: BigDecimal? = null)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/ForbiddenPair.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/ForbiddenPair.kt
new file mode 100644
index 000000000..9dbbbf16d
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/ForbiddenPair.kt
@@ -0,0 +1,10 @@
+package co.nilin.opex.api.core.inout.otc
+
+data class ForbiddenPair(
+ val sourceSymbol: String,
+ val destinationSymbol: String
+)
+
+data class ForbiddenPairs(
+ var forbiddenPairs: List?
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Rate.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Rate.kt
new file mode 100644
index 000000000..f1b9f3646
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Rate.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.inout.otc
+
+import java.math.BigDecimal
+
+data class Rate(
+ val sourceSymbol: String,
+ val destSymbol: String,
+ val rate: BigDecimal
+)
+
+data class Rates(
+ var rates: List?
+)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/SetCurrencyExchangeRateRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/SetCurrencyExchangeRateRequest.kt
new file mode 100644
index 000000000..ed2ee64c5
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/SetCurrencyExchangeRateRequest.kt
@@ -0,0 +1,18 @@
+package co.nilin.opex.api.core.inout.otc
+
+import co.nilin.opex.common.OpexError
+import java.math.BigDecimal
+
+class SetCurrencyExchangeRateRequest(
+ val sourceSymbol: String,
+ val destSymbol: String,
+ val rate: BigDecimal,
+ var ignoreIfExist: Boolean? = false
+) {
+ fun validate() {
+ if (rate <= BigDecimal.ZERO)
+ throw OpexError.InvalidRate.exception()
+ else if (sourceSymbol == destSymbol)
+ throw OpexError.SourceIsEqualDest.exception()
+ }
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Symbols.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Symbols.kt
new file mode 100644
index 000000000..2ae155ad7
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Symbols.kt
@@ -0,0 +1,3 @@
+package co.nilin.opex.api.core.inout.otc
+
+data class Symbols(var symbols: List?)
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyService.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyService.kt
index f5329c6fa..580e40a0a 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyService.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyService.kt
@@ -1,24 +1,58 @@
package co.nilin.opex.api.core.spi
-import co.nilin.opex.api.core.inout.APIKey
-import java.time.LocalDateTime
-
interface APIKeyService {
- suspend fun createAPIKey(
- userId: String,
- label: String,
- expirationTime: LocalDateTime?,
- allowedIPs: String?,
- currentToken: String
- ): Pair
-
- suspend fun getAPIKey(key: String, secret: String): APIKey?
-
- suspend fun getKeysByUserId(userId: String): List
-
- suspend fun changeKeyState(userId: String, key: String, isEnabled: Boolean)
-
- suspend fun deleteKey(userId: String, key: String)
-
+ data class ApiKeyRecord(
+ val apiKeyId: String,
+ val label: String?,
+ val enabled: Boolean,
+ val allowedIps: Set?,
+ val allowedEndpoints: Set?,
+ val keycloakUserId: String?,
+ val keycloakUsername: String?
+ )
+
+ data class ApiKeyCreateResult(
+ val secret: String,
+ val record: ApiKeyRecord
+ )
+
+ data class ApiKeyVerification(
+ val apiKeyId: String,
+ val secret: String,
+ val enabled: Boolean,
+ val allowedEndpoints: Set?,
+ val allowedIps: Set?,
+ val keycloakUserId: String?
+ )
+
+ suspend fun createApiKeyRecord(
+ apiKeyId: String,
+ label: String?,
+ plaintextSecret: String,
+ allowedIps: Set?,
+ allowedEndpoints: Set?,
+ keycloakUserId: String?,
+ keycloakUsername: String?,
+ enabled: Boolean
+ ): ApiKeyCreateResult
+
+ suspend fun rotateApiKeySecret(apiKeyId: String, newPlaintextSecret: String): ApiKeyCreateResult
+
+ suspend fun updateApiKeyRecord(
+ apiKeyId: String,
+ label: String?,
+ enabled: Boolean?,
+ allowedIps: Set?,
+ allowedEndpoints: Set?,
+ keycloakUsername: String?
+ ): ApiKeyRecord
+
+ suspend fun getApiKeyRecord(apiKeyId: String): ApiKeyRecord?
+
+ suspend fun listApiKeyRecords(): List
+
+ suspend fun deleteApiKeyRecord(apiKeyId: String)
+
+ suspend fun getApiKeyForVerification(apiKeyId: String): ApiKeyVerification?
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt
index c6a32e2ab..1497d7a32 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt
@@ -1,14 +1,29 @@
package co.nilin.opex.api.core.spi
-import co.nilin.opex.api.core.inout.PairFeeResponse
-import co.nilin.opex.api.core.inout.PairInfoResponse
+import co.nilin.opex.api.core.inout.FeeConfig
+import co.nilin.opex.api.core.inout.PairConfigResponse
+import co.nilin.opex.api.core.inout.UserFee
+import co.nilin.opex.api.core.inout.WithdrawLimitConfig
+import co.nilin.opex.api.core.inout.analytics.DailyAmount
+import co.nilin.opex.common.utils.Interval
+import java.math.BigDecimal
interface AccountantProxy {
- suspend fun getPairConfigs(): List
+ suspend fun getPairConfigs(): List
- suspend fun getFeeConfigs(): List
+ suspend fun getFeeConfigs(): List
- suspend fun getFeeConfig(symbol: String): PairFeeResponse
+ suspend fun getUserFee(uuid: String): UserFee
+ suspend fun getTradeVolumeByCurrency(uuid: String, symbol: String, interval: Interval): BigDecimal
+
+ suspend fun getTotalTradeVolumeValue(uuid: String, interval: Interval): BigDecimal
+
+ suspend fun getWithdrawLimitConfigs(): List
+
+ suspend fun getTotalWithdrawVolumeValue(uuid: String, interval: Interval?): BigDecimal
+ suspend fun getDailyDepositLast31Days(uuid: String): List
+ suspend fun getDailyWithdrawLast31Days(uuid: String): List
+ suspend fun getDailyTradeLast31Days(uuid: String): List
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ApiKeySecretCrypto.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ApiKeySecretCrypto.kt
new file mode 100644
index 000000000..d5e164cbe
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ApiKeySecretCrypto.kt
@@ -0,0 +1,6 @@
+package co.nilin.opex.api.core.spi
+
+interface ApiKeySecretCrypto {
+ fun encrypt(plaintext: String): String
+ fun decrypt(ciphertext: String): String
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AuthProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AuthProxy.kt
new file mode 100644
index 000000000..fb1d6c3f1
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AuthProxy.kt
@@ -0,0 +1,25 @@
+package co.nilin.opex.api.core.spi
+
+import co.nilin.opex.api.core.inout.auth.*
+
+interface AuthProxy {
+
+ suspend fun requestGetToken(request: PasswordFlowTokenRequest): TokenResponse
+ suspend fun confirmGetToken(request: ConfirmPasswordFlowTokenRequest): TokenResponse
+ suspend fun resendLoginOtp(request: ResendOtpRequest, token: String): ResendOtpResponse
+ suspend fun getToken(request: ExternalIdpTokenRequest): TokenResponse
+ suspend fun refreshToken(request: RefreshTokenRequest): TokenResponse
+ suspend fun registerUser(request: RegisterUserRequest): TempOtpResponse
+ suspend fun verifyRegister(request: VerifyOTPRequest): OTPActionTokenResponse
+ suspend fun confirmRegister(request: ConfirmRegisterRequest): Token
+ suspend fun registerExternalIdpUser(request: ExternalIdpUserRegisterRequest): TokenResponse
+ suspend fun forgetPassword(request: ForgotPasswordRequest): TempOtpResponse
+ suspend fun verifyForget(request: VerifyOTPRequest): OTPActionTokenResponse
+ suspend fun confirmForget(request: ConfirmForgetRequest)
+ suspend fun logout(token: String)
+ suspend fun logout(sessionId: String, token: String)
+ suspend fun getSessions(request: SessionRequest, token: String): List
+ suspend fun logoutOthers(token: String)
+ suspend fun logoutAll(token: String)
+
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt
index cd99d1064..2929c5f96 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/BlockchainGatewayProxy.kt
@@ -1,15 +1,24 @@
package co.nilin.opex.api.core.spi
+import co.nilin.opex.api.core.inout.AssignAddressRequest
import co.nilin.opex.api.core.inout.AssignResponse
-import co.nilin.opex.api.core.inout.CurrencyImplementation
+import co.nilin.opex.api.core.inout.ChainInfo
import co.nilin.opex.api.core.inout.DepositDetails
+import co.nilin.opex.api.core.inout.GatewayLocalizationCommand
+import co.nilin.opex.api.core.inout.GatewayLocalizationResponse
interface BlockchainGatewayProxy {
- suspend fun assignAddress(uuid: String, currency: String, chain: String): AssignResponse?
-
+ suspend fun assignAddress(assignAddressRequest: AssignAddressRequest): AssignResponse?
suspend fun getDepositDetails(refs: List): List
+ suspend fun getChainInfo(): List
- suspend fun getCurrencyImplementations(currency: String? = null): List
+ suspend fun getOnChainGatewayLocalizations(token: String, gatewayUuid: String): GatewayLocalizationResponse
+ suspend fun saveOnChainGatewayLocalizations(
+ token: String,
+ gatewayUuid: String,
+ gatewayLocalizations: List
+ ): GatewayLocalizationResponse
+ suspend fun deleteOnChainGatewayLocalization(token: String, id: Long)
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ConfigProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ConfigProxy.kt
new file mode 100644
index 000000000..7d9753344
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ConfigProxy.kt
@@ -0,0 +1,21 @@
+package co.nilin.opex.api.core.spi
+
+import co.nilin.opex.api.core.inout.UpdateUserConfigRequest
+import co.nilin.opex.api.core.inout.UpdateWebConfigRequest
+import co.nilin.opex.api.core.inout.UserLevelConfig
+import co.nilin.opex.api.core.inout.UserWebConfig
+import co.nilin.opex.common.data.WebConfig
+
+interface ConfigProxy {
+ suspend fun getWebConfig(): WebConfig
+ suspend fun updateWebConfig(token: String, request: UpdateWebConfigRequest): WebConfig
+ suspend fun getUserLevelConfig(): List
+ suspend fun updateUserLevelConfig(token: String, userLevelConfig: UserLevelConfig): UserLevelConfig
+ suspend fun deleteUserLevelConfig(token: String, userLevel: String, language: String)
+ suspend fun getUserConfig(token: String): UserWebConfig
+ suspend fun updateUserConfig(token: String, request: UpdateUserConfigRequest): UserWebConfig
+ suspend fun getUserFavoritePair(token: String): Set
+ suspend fun addUserFavoritePair(token: String, pair: String): Set
+ suspend fun removeUserFavoritePair(token: String, pair: String): Set
+
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ManualRateLimiterService.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ManualRateLimiterService.kt
new file mode 100644
index 000000000..9858315d1
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ManualRateLimiterService.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.core.spi
+
+import co.nilin.opex.api.core.inout.ManualRateLimitGroupType
+import org.springframework.web.server.ServerWebExchange
+
+interface ManualRateLimiterService {
+ fun check(
+ identity: String,
+ group: ManualRateLimitGroupType,
+ exchange: ServerWebExchange
+ )
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt
index 00b3d2abf..f43b276d6 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketDataProxy.kt
@@ -2,7 +2,6 @@ package co.nilin.opex.api.core.spi
import co.nilin.opex.api.core.inout.*
import co.nilin.opex.common.utils.Interval
-import java.time.LocalDateTime
interface MarketDataProxy {
@@ -18,6 +17,10 @@ interface MarketDataProxy {
suspend fun recentTrades(symbol: String, limit: Int): List
+ suspend fun recentTrades(token: String, request: AdminTradesHistoryRequest): List
+
+ suspend fun recentOrders(token: String, request: AdminOrdersHistoryRequest): List
+
suspend fun lastPrice(symbol: String?): List
suspend fun getBestPriceForSymbols(symbols: List): List
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt
index 7a4e6f59b..c5780e2b0 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MarketUserDataProxy.kt
@@ -1,18 +1,21 @@
package co.nilin.opex.api.core.spi
+import co.nilin.opex.api.core.inout.MatchingOrderType
import co.nilin.opex.api.core.inout.Order
+import co.nilin.opex.api.core.inout.OrderData
+import co.nilin.opex.api.core.inout.OrderDirection
import co.nilin.opex.api.core.inout.Trade
import java.security.Principal
import java.util.*
interface MarketUserDataProxy {
- suspend fun queryOrder(principal: Principal, symbol: String, orderId: Long?, origClientOrderId: String?): Order?
+ suspend fun queryOrder(token: String, symbol: String, orderId: Long?, origClientOrderId: String?): Order?
- suspend fun openOrders(principal: Principal, symbol: String?, limit: Int?): List
+ suspend fun openOrders(token: String, symbol: String?, limit: Int?): List
suspend fun allOrders(
- principal: Principal,
+ token: String,
symbol: String?,
startTime: Date?,
endTime: Date?,
@@ -20,11 +23,49 @@ interface MarketUserDataProxy {
): List
suspend fun allTrades(
- principal: Principal,
+ token: String,
symbol: String?,
fromTrade: Long?,
startTime: Date?,
endTime: Date?,
limit: Int?
): List
+
+ suspend fun getOrderHistory(
+ token: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List
+
+ suspend fun getOrderHistoryCount(
+ token: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ orderType: MatchingOrderType?,
+ direction: OrderDirection?,
+ ): Long
+
+ suspend fun getTradeHistory(
+ token: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ direction: OrderDirection?,
+ limit: Int?,
+ offset: Int?,
+ ): List
+
+ suspend fun getTradeHistoryCount(
+ token: String,
+ symbol: String?,
+ startTime: Long?,
+ endTime: Long?,
+ direction: OrderDirection?,
+ ): Long
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt
index 906aec319..c615e3d3b 100644
--- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MatchingGatewayProxy.kt
@@ -4,6 +4,8 @@ import co.nilin.opex.api.core.inout.MatchConstraint
import co.nilin.opex.api.core.inout.MatchingOrderType
import co.nilin.opex.api.core.inout.OrderDirection
import co.nilin.opex.api.core.inout.OrderSubmitResult
+import co.nilin.opex.api.core.inout.PairConfigResponse
+import co.nilin.opex.api.core.inout.PairSetting
import java.math.BigDecimal
interface MatchingGatewayProxy {
@@ -27,4 +29,6 @@ interface MatchingGatewayProxy {
symbol: String,
token: String?
): OrderSubmitResult?
+
+ suspend fun getPairSettings(): List
}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ProfileProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ProfileProxy.kt
new file mode 100644
index 000000000..d5d08aef0
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/ProfileProxy.kt
@@ -0,0 +1,43 @@
+package co.nilin.opex.api.core.spi
+
+import co.nilin.opex.api.core.inout.CompleteProfileRequest
+import co.nilin.opex.api.core.inout.ContactUpdateConfirmRequest
+import co.nilin.opex.api.core.inout.ContactUpdateRequest
+import co.nilin.opex.api.core.inout.ProfileApprovalRequestUser
+import co.nilin.opex.api.core.inout.*
+
+interface ProfileProxy {
+ suspend fun getProfile(token: String): Profile
+ suspend fun completeProfile(token: String, request: CompleteProfileRequest): Profile?
+ suspend fun requestContactUpdate(token: String, request: ContactUpdateRequest): TempOtpResponse
+ suspend fun confirmContactUpdate(token: String, request: ContactUpdateConfirmRequest)
+ suspend fun getUserProfileApprovalRequest(token: String): ProfileApprovalRequestUser
+
+ // Admin
+ suspend fun getProfiles(token: String, profileRequest: ProfileRequest): List
+ suspend fun getProfileAdmin(token: String, uuid: String): Profile
+ suspend fun getProfileHistory(token: String, uuid: String, limit: Int, offset: Int): List
+ suspend fun getProfileApprovalRequests(
+ token: String,
+ request: ProfileApprovalRequestFilter
+ ): List
+
+ suspend fun getProfileApprovalRequest(token: String, requestId: Long): ProfileApprovalAdminResponse
+ suspend fun updateProfileApprovalRequest(
+ token: String,
+ request: UpdateApprovalRequestBody
+ ): ProfileApprovalAdminResponse
+
+ suspend fun resolveUsers(token: String, request: ResolveUsersRequest): Map
+
+ // Address Book
+ suspend fun addAddressBook(token: String, request: AddAddressBookItemRequest): AddressBookResponse
+ suspend fun getAllAddressBooks(token: String): List
+ suspend fun deleteAddressBook(token: String, id: Long)
+ suspend fun updateAddressBook(token: String, id: Long, request: AddAddressBookItemRequest): AddressBookResponse
+
+ //Bank Account
+ suspend fun addBankAccount(token: String, request: AddBankAccountRequest): BankAccountResponse
+ suspend fun getBankAccounts(token: String): List
+ suspend fun deleteBankAccount(token: String, id: Long)
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateLimitConfigService.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateLimitConfigService.kt
new file mode 100644
index 000000000..20971f307
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateLimitConfigService.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.core.spi
+
+import co.nilin.opex.api.core.inout.RateLimitEndpoint
+import co.nilin.opex.api.core.inout.RateLimitGroup
+import co.nilin.opex.api.core.inout.RateLimitPenalty
+
+interface RateLimitConfigService {
+ suspend fun loadConfig()
+ fun getGroup(groupId: Long): RateLimitGroup?
+ fun getGroup(groupName: String): RateLimitGroup?
+ fun getPenalties(groupId: Long): List
+ fun getEndpoints(): List
+}
\ No newline at end of file
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateProxy.kt
new file mode 100644
index 000000000..1be97ce42
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateProxy.kt
@@ -0,0 +1,33 @@
+package co.nilin.opex.api.core.spi
+
+import co.nilin.opex.api.core.inout.otc.*
+
+interface RateProxy {
+ // Rates (writes require admin token)
+ suspend fun createRate(token: String, request: SetCurrencyExchangeRateRequest)
+ suspend fun updateRate(token: String, request: SetCurrencyExchangeRateRequest): Rates
+ suspend fun deleteRate(token: String, sourceSymbol: String, destSymbol: String): Rates
+
+ // Rates (reads are public)
+ suspend fun fetchRates(): Rates
+ suspend fun fetchRate(sourceSymbol: String, destSymbol: String): Rate?
+
+ // Forbidden pairs
+ suspend fun addForbiddenPair(token: String, request: CurrencyPair)
+ suspend fun deleteForbiddenPair(token: String, sourceSymbol: String, destSymbol: String): ForbiddenPairs
+
+ // Forbidden pairs (read is public)
+ suspend fun fetchForbiddenPairs(): ForbiddenPairs
+
+ // Transitive symbols
+ suspend fun addTransitiveSymbols(token: String, symbols: Symbols)
+ suspend fun deleteTransitiveSymbol(token: String, symbol: String): Symbols
+ suspend fun deleteTransitiveSymbols(token: String, symbols: Symbols): Symbols
+
+ // Transitive symbols (read is public)
+ suspend fun fetchTransitiveSymbols(): Symbols
+
+ // Routes and prices (reads are public)
+ suspend fun fetchRoutes(sourceSymbol: String? = null, destSymbol: String? = null): CurrencyExchangeRatesResponse
+ suspend fun getPrice(unit: String): List
+}
diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt
new file mode 100644
index 000000000..fe29d4f0e
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/StorageProxy.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.core.spi
+
+import org.springframework.http.ResponseEntity
+import org.springframework.http.codec.multipart.FilePart
+
+interface StorageProxy {
+ suspend fun adminDownload(token: String, bucket: String, key: String): ResponseEntity