diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml
index b2a2e11aa..164918767 100644
--- a/api/api-app/pom.xml
+++ b/api/api-app/pom.xml
@@ -74,6 +74,15 @@
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/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/RateLimitConfig.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfig.kt
new file mode 100644
index 000000000..f56888aef
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/RateLimitConfig.kt
@@ -0,0 +1,104 @@
+package co.nilin.opex.api.app.config
+
+import co.nilin.opex.api.app.service.RateLimitCoordinatorService
+import co.nilin.opex.api.core.inout.RateLimitEndpoint
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import org.springframework.http.HttpStatus
+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 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)
+ }
+
+
+ private fun applyRateLimitIfAuthenticated(
+ exchange: ServerWebExchange,
+ chain: WebFilterChain,
+ endpoint: RateLimitEndpoint
+ ): Mono {
+
+ return ReactiveSecurityContextHolder.getContext()
+ .mapNotNull { it.authentication }
+ .filter { it.isAuthenticated }
+ .flatMap { auth ->
+ if (auth != null && !auth.name.isNullOrBlank())
+ applyRateLimit(auth.name, exchange, chain, endpoint)
+ else
+ chain.filter(exchange)
+ }
+
+ }
+
+
+ private fun applyRateLimit(
+ identity: String,
+ exchange: ServerWebExchange,
+ chain: WebFilterChain,
+ endpoint: RateLimitEndpoint
+ ): Mono {
+
+ val group = rateLimitConfig.getGroup(endpoint.groupId)
+ ?: return chain.filter(exchange)
+
+ val result = coordinator.check(
+ identity = identity,
+ groupId = endpoint.groupId,
+ maxRequests = group.requestCount,
+ windowSeconds = group.requestWindowSeconds,
+ apiPath = endpoint.url,
+ apiMethod = endpoint.method
+ )
+
+ return if (result.blocked) {
+ tooManyRequests(exchange, identity, endpoint.url, endpoint.method, result.retryAfterSeconds)
+ } else {
+ chain.filter(exchange)
+ }
+ }
+
+ //TODO should throw opex error
+ private fun tooManyRequests(
+ exchange: ServerWebExchange,
+ identity: String,
+ url: String,
+ method: String,
+ retryAfterSeconds: Int
+ ): Mono {
+ exchange.response.statusCode = HttpStatus.TOO_MANY_REQUESTS
+ return exchange.response.writeWith(
+ Mono.just(
+ exchange.response.bufferFactory()
+ .wrap("Rate limit exceeded ($identity) -- $method:$url -- Retry-After, $retryAfterSeconds".toByteArray())
+ )
+ )
+ }
+}
\ 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/controller/RateLimitController.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
new file mode 100644
index 000000000..e4a8f679c
--- /dev/null
+++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/RateLimitController.kt
@@ -0,0 +1,17 @@
+package co.nilin.opex.api.app.controller
+
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+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")
+class RateLimitController(
+ private val rateLimitConfig: RateLimitConfigService,
+) {
+ @PostMapping
+ suspend fun reloadRateLimits() {
+ rateLimitConfig.loadConfig()
+ }
+}
\ 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/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/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 80a1b7cc9..8b654ad05 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:
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/spi/RateLimitConfigService.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateLimitConfigService.kt
new file mode 100644
index 000000000..c8fc9fe2b
--- /dev/null
+++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateLimitConfigService.kt
@@ -0,0 +1,12 @@
+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 getPenalties(groupId: Long): List
+ fun getEndpoints(): List
+}
\ No newline at end of file
diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt
index 972a90826..8ebdb1767 100644
--- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt
+++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt
@@ -35,6 +35,7 @@ class SecurityConfig(
it.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/swagger-ui/**").permitAll()
.pathMatchers("/swagger-resources/**").permitAll()
+ .pathMatchers("/v1/rate-limit").hasAuthority("ROLE_admin")
.pathMatchers("/v2/api-docs").permitAll()
.pathMatchers("/v3/depth").permitAll()
.pathMatchers("/v3/trades").permitAll()
diff --git a/api/api-ports/api-persister-postgres/pom.xml b/api/api-ports/api-persister-postgres/pom.xml
index 695c5d793..02746c921 100644
--- a/api/api-ports/api-persister-postgres/pom.xml
+++ b/api/api-ports/api-persister-postgres/pom.xml
@@ -67,5 +67,17 @@
io.mockk
mockk
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.flywaydb
+ flyway-core
+
+
+ org.flywaydb
+ flyway-database-postgresql
+
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt
index 47e7038ae..8b3551fba 100644
--- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/config/PostgresConfig.kt
@@ -1,24 +1,55 @@
package co.nilin.opex.api.ports.postgres.config
+import org.flywaydb.core.Flyway
+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.io.Resource
+import org.springframework.context.annotation.Profile
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
-import org.springframework.r2dbc.core.DatabaseClient
@Configuration
@EnableR2dbcRepositories(basePackages = ["co.nilin.opex"])
-class PostgresConfig(
- db: DatabaseClient,
- @Value("classpath:schema.sql") private val schemaResource: Resource
-) {
+@Profile("!test")
+class PostgresConfig {
+ private val logger = LoggerFactory.getLogger(PostgresConfig::class.java)
+
init {
- val schemaReader = schemaResource.inputStream.reader()
- val schema = schemaReader.readText().trim()
- schemaReader.close()
- val initDb = db.sql { schema }
- initDb // initialize the database
- .then()
- .subscribe() // execute
+ logger.info("🔍 PostgresConfig loaded")
+ }
+
+ @Bean
+ fun flywayConfig(
+ @Value("\${spring.datasource.url}") url: String,
+ @Value("\${spring.datasource.username}") user: String,
+ @Value("\${spring.datasource.password}") password: String
+ ): Flyway? {
+ val flyway: Flyway = Flyway.configure()
+ .dataSource(url, user, password)
+ .locations("classpath:db/migration")
+ .baselineOnMigrate(true)
+ .baselineVersion("1")
+ .load()
+ try {
+ retry(6, 5000) {
+ flyway.migrate()
+ }
+ } catch (e: Exception) {
+ logger.error("❌ Flyway migration failed", e)
+ }
+ return flyway
+ }
+
+ fun retry(times: Int, delayMs: Long, block: () -> Unit) {
+ var attempt = 0
+ while (true) {
+ try {
+ block()
+ return
+ } catch (e: Exception) {
+ if (++attempt >= times) throw e
+ Thread.sleep(delayMs)
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitEndpointRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitEndpointRepository.kt
new file mode 100644
index 000000000..aca1a7fc2
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitEndpointRepository.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.ports.postgres.dao
+
+import co.nilin.opex.api.ports.postgres.model.RateLimitEndpointModel
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+
+@Repository
+interface RateLimitEndpointRepository : ReactiveCrudRepository {
+ fun findByEnabledTrue(): Flux
+
+}
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitGroupRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitGroupRepository.kt
new file mode 100644
index 000000000..3b7dfcc76
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitGroupRepository.kt
@@ -0,0 +1,12 @@
+package co.nilin.opex.api.ports.postgres.dao
+
+import co.nilin.opex.api.ports.postgres.model.RateLimitGroupModel
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+
+@Repository
+interface RateLimitGroupRepository : ReactiveCrudRepository {
+ fun findByEnabledTrue(): Flux
+
+}
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitPenaltyRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitPenaltyRepository.kt
new file mode 100644
index 000000000..a84d8c95a
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/RateLimitPenaltyRepository.kt
@@ -0,0 +1,11 @@
+package co.nilin.opex.api.ports.postgres.dao
+
+import co.nilin.opex.api.ports.postgres.model.RateLimitPenaltyModel
+import org.springframework.data.repository.reactive.ReactiveCrudRepository
+import org.springframework.stereotype.Repository
+import reactor.core.publisher.Flux
+
+@Repository
+interface RateLimitPenaltyRepository : ReactiveCrudRepository {
+ fun findByGroupIdOrderByBlockStepAsc(groupId: Long): Flux
+}
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/RateLimitConfigImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/RateLimitConfigImpl.kt
new file mode 100644
index 000000000..4f21a7139
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/RateLimitConfigImpl.kt
@@ -0,0 +1,70 @@
+package co.nilin.opex.api.ports.postgres.impl
+
+import co.nilin.opex.api.core.inout.RateLimitEndpoint
+import co.nilin.opex.api.core.inout.RateLimitGroup
+import co.nilin.opex.api.core.inout.RateLimitPenalty
+import co.nilin.opex.api.core.spi.RateLimitConfigService
+import co.nilin.opex.api.ports.postgres.dao.RateLimitEndpointRepository
+import co.nilin.opex.api.ports.postgres.dao.RateLimitGroupRepository
+import co.nilin.opex.api.ports.postgres.dao.RateLimitPenaltyRepository
+import co.nilin.opex.api.ports.postgres.model.RateLimitEndpointModel
+import co.nilin.opex.api.ports.postgres.model.RateLimitGroupModel
+import co.nilin.opex.api.ports.postgres.model.RateLimitPenaltyModel
+import kotlinx.coroutines.reactive.awaitFirstOrElse
+import org.springframework.stereotype.Component
+
+@Component
+class RateLimitConfigImpl(
+ private val groupRepo: RateLimitGroupRepository,
+ private val penaltyRepo: RateLimitPenaltyRepository,
+ private val endpointRepo: RateLimitEndpointRepository
+) : RateLimitConfigService {
+
+ private val groupCache = mutableMapOf()
+ private val penaltyCache = mutableMapOf>()
+ private val endpointCache = mutableListOf()
+
+ override suspend fun loadConfig() {
+ val groups = groupRepo.findByEnabledTrue().collectList().awaitFirstOrElse { emptyList() }
+ groupCache.clear()
+ groups.forEach { groupCache[it.id!!] = it.toRateLimitGroup() }
+
+ penaltyCache.clear()
+ groups.forEach { group ->
+ val penalties = penaltyRepo.findByGroupIdOrderByBlockStepAsc(group.id!!).collectList()
+ .awaitFirstOrElse { emptyList() }.map { it.toRateLimitPenalty() }
+ penaltyCache[group.id] = penalties
+ }
+
+ endpointCache.clear()
+ endpointCache.addAll(endpointRepo.findByEnabledTrue().collectList().awaitFirstOrElse { emptyList() }
+ .map { it.toRateLimitEndpoint() })
+ }
+
+ override fun getGroup(groupId: Long): RateLimitGroup? = groupCache[groupId]
+ override fun getPenalties(groupId: Long): List = penaltyCache[groupId] ?: emptyList()
+ override fun getEndpoints(): List = endpointCache
+
+
+ private fun RateLimitGroupModel.toRateLimitGroup(): RateLimitGroup =
+ RateLimitGroup(id, name, requestCount, requestWindowSeconds, cooldownSeconds, maxPenaltyLevel, enabled)
+
+
+ private fun RateLimitPenaltyModel.toRateLimitPenalty(): RateLimitPenalty =
+ RateLimitPenalty(
+ id,
+ groupId,
+ blockStep,
+ blockDurationSeconds
+ )
+
+ private fun RateLimitEndpointModel.toRateLimitEndpoint(): RateLimitEndpoint =
+ RateLimitEndpoint(
+ id,
+ url,
+ method,
+ groupId,
+ priority,
+ enabled
+ )
+}
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitEndpointModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitEndpointModel.kt
new file mode 100644
index 000000000..f8e0368b1
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitEndpointModel.kt
@@ -0,0 +1,15 @@
+package co.nilin.opex.api.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table(name = "rate_limit_endpoint")
+data class RateLimitEndpointModel(
+ @Id
+ 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-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitGroupModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitGroupModel.kt
new file mode 100644
index 000000000..506687988
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitGroupModel.kt
@@ -0,0 +1,16 @@
+package co.nilin.opex.api.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table(name = "rate_limit_group")
+data class RateLimitGroupModel(
+ @Id
+ 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-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitPenaltyModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitPenaltyModel.kt
new file mode 100644
index 000000000..82cd01fbc
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/RateLimitPenaltyModel.kt
@@ -0,0 +1,13 @@
+package co.nilin.opex.api.ports.postgres.model
+
+import org.springframework.data.annotation.Id
+import org.springframework.data.relational.core.mapping.Table
+
+@Table(name = "rate_limit_penalty")
+data class RateLimitPenaltyModel(
+ @Id
+ val id: Long? = null,
+ val groupId: Long,
+ val blockStep: Int,
+ val blockDurationSeconds: Int
+)
diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/RedisCacheHelper.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/RedisCacheHelper.kt
new file mode 100644
index 000000000..cde37154e
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/RedisCacheHelper.kt
@@ -0,0 +1,104 @@
+package co.nilin.opex.api.ports.postgres.util
+
+import co.nilin.opex.common.utils.DynamicInterval
+import co.nilin.opex.common.utils.LoggerDelegate
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.stereotype.Component
+
+@Component
+class RedisCacheHelper(private val redisTemplate: RedisTemplate) {
+
+ private val logger by LoggerDelegate()
+
+ private val valueOps = redisTemplate.opsForValue()
+ private val listOps = redisTemplate.opsForList()
+
+ fun put(key: String, value: Any?, expireAt: DynamicInterval? = null) {
+ value ?: return
+ try {
+ valueOps.set(key, value)
+ expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) }
+ } catch (e: Exception) {
+ logger.warn("Unable to put cache with key '$key'")
+ }
+ }
+
+ fun putList(key: String, values: List, expireAt: DynamicInterval? = null) {
+ try {
+ values.forEach { listOps.rightPush(key, it) }
+ expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) }
+ } catch (e: Exception) {
+ logger.warn("Unable to put list cache with key '$key'")
+ }
+ }
+
+ fun putListItem(key: String, value: Any, rightPush: Boolean = true) {
+ try {
+ if (rightPush)
+ listOps.rightPush(key, value)
+ else
+ listOps.leftPush(key, value)
+ } catch (e: Exception) {
+ logger.warn("Unable to put list item cache with key '$key'")
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun get(key: String): T? {
+ return try {
+ valueOps.get(key) as T
+ } catch (e: Exception) {
+ logger.warn("Unable to get cache value with key '$key'")
+ null
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun getList(key: String): Collection? {
+ return try {
+ listOps.range(key, 0, -1) as Collection?
+ } catch (e: Exception) {
+ logger.warn("Unable to get cache list with key '$key'")
+ null
+ }
+ }
+
+ suspend fun getOrElse(key: String, expireAt: DynamicInterval? = null, action: suspend () -> T): T {
+ val cacheValue = get(key)
+ return if (cacheValue != null)
+ cacheValue
+ else {
+ val value = action()
+ if (value != null) {
+ put(key, value)
+ expireAt?.let { setExpiration(key, it) }
+ }
+ return value
+ }
+ }
+
+ fun evict(key: String) {
+ try {
+ redisTemplate.delete(key)
+ } catch (e: Exception) {
+ logger.warn("Unable to evict cache with key '$key'")
+ }
+ }
+
+ fun setExpiration(key: String, interval: DynamicInterval) {
+ try {
+ redisTemplate.expireAt(key, interval.dateInFuture())
+ } catch (e: Exception) {
+ logger.warn("Unable to set expiration date for cache with key '$key'")
+ }
+ }
+
+ fun hasKey(key: String): Boolean {
+ return try {
+ redisTemplate.hasKey(key)
+ } catch (e: Exception) {
+ logger.warn("Unable fetch info of cache with key '$key'")
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V1__init_schema.sql b/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V1__init_schema.sql
new file mode 100644
index 000000000..d762a3f5b
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V1__init_schema.sql
@@ -0,0 +1 @@
+CREATE TABLE IF NOT EXISTS test(id SERIAL PRIMARY KEY);
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V2__create_tables.sql b/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V2__create_tables.sql
new file mode 100644
index 000000000..0c954b9fe
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V2__create_tables.sql
@@ -0,0 +1,54 @@
+CREATE TABLE IF NOT EXISTS symbol_maps
+(
+ id SERIAL PRIMARY KEY,
+ symbol VARCHAR(72) NOT NULL,
+ alias_key VARCHAR(72) NOT NULL,
+ alias VARCHAR(72) NOT NULL,
+ UNIQUE (symbol, alias_key, alias)
+);
+
+DROP TABLE IF EXISTS api_key;
+
+CREATE TABLE IF NOT EXISTS api_key_registry
+(
+ api_key_id VARCHAR(128) PRIMARY KEY,
+ label VARCHAR(200),
+ encrypted_secret TEXT NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ allowed_ips TEXT,
+ allowed_endpoints TEXT,
+ keycloak_user_id VARCHAR(128),
+ keycloak_username VARCHAR(256),
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL
+);
+CREATE TABLE IF NOT EXISTS rate_limit_group
+(
+ id BIGSERIAL PRIMARY KEY,
+ name VARCHAR(50) NOT NULL,
+ request_count INT NOT NULL,
+ request_window_seconds INT NOT NULL,
+ cooldown_seconds INT NOT NULL,
+ max_penalty_level INT NOT NULL,
+ enabled BOOLEAN NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS rate_limit_penalty
+(
+ id BIGSERIAL PRIMARY KEY,
+ group_id BIGINT NOT NULL REFERENCES rate_limit_group (id),
+ block_step INT NOT NULL,
+ block_duration_seconds INT NOT NULL,
+ unique (group_id, block_step)
+);
+
+CREATE TABLE IF NOT EXISTS rate_limit_endpoint
+(
+ id BIGSERIAL PRIMARY KEY,
+ url VARCHAR(255) NOT NULL,
+ method VARCHAR(10) NOT NULL,
+ group_id BIGINT NOT NULL REFERENCES rate_limit_group (id),
+ priority INT NOT NULL,
+ enabled BOOLEAN NOT NULL,
+ unique (url, method)
+);
diff --git a/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V3__insert_rate_limit_data.sql b/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V3__insert_rate_limit_data.sql
new file mode 100644
index 000000000..c255b5e81
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/main/resources/db/migration/V3__insert_rate_limit_data.sql
@@ -0,0 +1,92 @@
+-- ________________ Admin Services ________________
+INSERT INTO rate_limit_group (id, name, request_count, request_window_seconds, cooldown_seconds, max_penalty_level,
+ enabled)
+VALUES (1, 'ADMIN', 15, 30, 120, 1, true);
+
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (1, 1, 60);
+
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/admin/**', 'GET', 1, true, 1000);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/admin/**', 'POST', 1, true, 1000);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/admin/**', 'PUT', 1, true, 1000);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/admin/**', 'DELETE', 1, true, 1000);
+-- __________________________________________________________________________________________________________________________
+
+INSERT INTO rate_limit_group (id, name, request_count, request_window_seconds, cooldown_seconds, max_penalty_level,
+ enabled)
+VALUES (2, 'HIGH_IMPACT', 5, 60, 600, 3, true);
+
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (2, 3, 300);
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (2, 2, 180);
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (2, 1, 120);
+
+
+INSERT INTO rate_limit_group (id, name, request_count, request_window_seconds, cooldown_seconds, max_penalty_level,
+ enabled)
+VALUES (3, 'LOW_IMPACT', 10, 60, 300, 3, true);
+
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (3, 3, 180);
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (3, 2, 120);
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (3, 1, 60);
+
+INSERT INTO rate_limit_group (id, name, request_count, request_window_seconds, cooldown_seconds, max_penalty_level,
+ enabled)
+VALUES (4, 'BOT', 100, 60, 120, 1, true);
+
+INSERT INTO rate_limit_penalty (group_id, block_step, block_duration_seconds)
+VALUES (4, 1, 60);
+
+
+-- WithdrawController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/withdraw/**', 'POST', 2, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/withdraw/**', 'PUT', 3, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/withdraw/**', 'GET', 3, true, 1);
+
+-- WalletController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/wallet/**', 'GET', 3, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/wallet/deposit/address', 'GET', 2, true, 2);
+
+-- VoucherController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/voucher/**', 'PUT', 2, true, 1);
+
+-- UserHistoryController && UserDataController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/user/**', 'GET', 3, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/user/**', 'POST', 3, true, 1);
+
+-- OrderController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/order/**', 'GET', 3, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/order/**', 'PUT', 3, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/order/**', 'POST', 3, true, 1);
+
+-- DepositController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/deposit/**', 'POST', 3, true, 1);
+
+-- RateController
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/otc/**', 'POST', 1, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/otc/**', 'PUT', 4, true, 1);
+INSERT INTO rate_limit_endpoint (url, method, group_id, enabled, priority)
+VALUES ('/opex/v1/otc/**', 'DELETE', 1, true, 1);
\ No newline at end of file
diff --git a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql
deleted file mode 100644
index e1a77a3bc..000000000
--- a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-CREATE TABLE IF NOT EXISTS symbol_maps
-(
- id SERIAL PRIMARY KEY,
- symbol VARCHAR(72) NOT NULL,
- alias_key VARCHAR(72) NOT NULL,
- alias VARCHAR(72) NOT NULL,
- UNIQUE (symbol, alias_key, alias)
-);
-DROP TABLE IF EXISTS api_key;
-
-CREATE TABLE IF NOT EXISTS api_key_registry
-(
- api_key_id VARCHAR(128) PRIMARY KEY,
- label VARCHAR(200),
- encrypted_secret TEXT NOT NULL,
- enabled BOOLEAN NOT NULL DEFAULT TRUE,
- allowed_ips TEXT,
- allowed_endpoints TEXT,
- keycloak_user_id VARCHAR(128),
- keycloak_username VARCHAR(256),
- created_at TIMESTAMP NOT NULL,
- updated_at TIMESTAMP NOT NULL
-);
diff --git a/docker-compose.yml b/docker-compose.yml
index d45da092e..fb352af93 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -220,7 +220,7 @@ services:
postgres-keycloak:
<<: *postgres-db
volumes:
- - keycloak-data-new:/var/lib/postgresql/data/
+ - keycloak-data:/var/lib/postgresql/data/
postgres-wallet:
<<: *postgres-db
volumes:
@@ -622,7 +622,7 @@ volumes:
accountant-data:
eventlog-data:
auth-data:
- keycloak-data-new:
+ keycloak-data:
wallet-data:
market-data:
api-data:
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/PaymentGatewayController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/PaymentGatewayController.kt
index 737c40e13..1e154a587 100644
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/PaymentGatewayController.kt
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/PaymentGatewayController.kt
@@ -41,7 +41,7 @@ class PaymentGatewayController(
val receiverWalletType = WalletType.MAIN
val currency =
- currencyService.fetchCurrency(FetchCurrency(symbol = request.currency.name))
+ currencyService.fetchCurrency(FetchCurrency(symbol = request.currency))
?: throw OpexError.CurrencyNotFound.exception()
val sourceOwner = walletOwnerManager.findWalletOwner(walletOwnerManager.systemUuid)
?: throw OpexError.WalletOwnerNotFound.exception()
@@ -82,7 +82,9 @@ class PaymentGatewayController(
depositType = DepositType.OFF_CHAIN,
network = null,
attachment = null,
- transferMethod = if (request.isIPG == true) TransferMethod.IPG else TransferMethod.MPG
+ transferMethod = if (request.transferMethod == TransferMethod.REWARD) TransferMethod.REWARD else {
+ if (request.isIPG == true) TransferMethod.IPG else TransferMethod.MPG
+ }
)
traceDepositService.saveDepositInNewTransaction(depositCommand)
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentCurrency.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentCurrency.kt
deleted file mode 100644
index 4b02a3a40..000000000
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentCurrency.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package co.nilin.opex.wallet.app.dto
-
-enum class PaymentCurrency {
- IRR, IRT
-}
\ No newline at end of file
diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentDepositRequest.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentDepositRequest.kt
index 304bc5fc7..20f4a1922 100644
--- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentDepositRequest.kt
+++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/PaymentDepositRequest.kt
@@ -1,12 +1,14 @@
package co.nilin.opex.wallet.app.dto
+import co.nilin.opex.wallet.core.inout.TransferMethod
import java.math.BigDecimal
data class PaymentDepositRequest(
val userId: String, // user uuid
val amount: BigDecimal,
- val currency: PaymentCurrency,
+ val currency: String,
val reference: String,
val description: String?,
- val isIPG: Boolean? = true
+ val isIPG: Boolean? = true,
+ val transferMethod: TransferMethod? = null
)
\ No newline at end of file
diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/TransferMethod.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/TransferMethod.kt
index b34d705d2..30fceb6d2 100644
--- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/TransferMethod.kt
+++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/TransferMethod.kt
@@ -1,5 +1,5 @@
package co.nilin.opex.wallet.core.inout
enum class TransferMethod {
- CARD, SHEBA, IPG, EXCHANGE , MANUALLY , VOUCHER, MPG
+ CARD, SHEBA, IPG, EXCHANGE , MANUALLY , VOUCHER, MPG , REWARD
}