From 9338fad4b9375201f6287db9433db84e0c6e2c6a Mon Sep 17 00:00:00 2001 From: Peyman Date: Sat, 29 Oct 2022 12:55:59 +0330 Subject: [PATCH 01/11] API key beta (#330) * Starting API key * Adding filter * Finish api key filter filter * Start caching * Finish caching --- api/api-app/pom.xml | 4 + .../kotlin/co/nilin/opex/api/app/ApiApp.kt | 1 + .../nilin/opex/api/app/config/CacheConfig.kt | 18 ++ .../api/app/controller/APIKeyController.kt | 64 ++++++ .../opex/api/app/data/APIKeyExpiration.kt | 22 ++ .../nilin/opex/api/app/data/APIKeyResponse.kt | 12 + .../opex/api/app/data/AccessTokenResponse.kt | 7 + .../opex/api/app/data/CreateAPIKeyRequest.kt | 7 + .../api/app/interceptor/APIKeyFilterImpl.kt | 34 +++ .../co/nilin/opex/api/app/proxy/AuthProxy.kt | 58 +++++ .../opex/api/app/service/APIKeyServiceImpl.kt | 209 ++++++++++++++++++ .../src/main/resources/application.yml | 3 + .../co/nilin/opex/api/core/inout/APIKey.kt | 14 ++ .../nilin/opex/api/core/spi/APIKeyFilter.kt | 3 + .../nilin/opex/api/core/spi/APIKeyService.kt | 24 ++ .../ports/binance/config/SecurityConfig.kt | 8 + .../ports/postgres/dao/APIKeyRepository.kt | 18 ++ .../api/ports/postgres/model/APIKeyModel.kt | 23 ++ .../ports/postgres/model/SymbolMapModel.kt | 1 - .../src/main/resources/schema.sql | 15 ++ docker-compose.yml | 1 + .../opex/utility/error/data/OpexError.kt | 1 + 22 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CacheConfig.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyExpiration.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/AccessTokenResponse.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt create mode 100644 api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/APIKey.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyFilter.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyService.kt create mode 100644 api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/APIKeyRepository.kt create mode 100644 api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml index 06d27bcd6..1d3cde562 100644 --- a/api/api-app/pom.xml +++ b/api/api-app/pom.xml @@ -27,6 +27,10 @@ org.springframework.boot spring-boot-starter + + org.springframework.boot + spring-boot-starter-cache + co.nilin.opex.utility.log logging-handler 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 ba5850a24..c323a9e19 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,6 +2,7 @@ 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 springfox.documentation.swagger2.annotations.EnableSwagger2 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 new file mode 100644 index 000000000..5127560d8 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/config/CacheConfig.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.api.app.config + +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 + +@Configuration +@EnableCaching +class CacheConfig { + + @Bean + fun apiKeyCacheManager(): CacheManager { + return ConcurrentMapCacheManager("apiKey") + } + +} \ 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 new file mode 100644 index 000000000..ab8bcd624 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/controller/APIKeyController.kt @@ -0,0 +1,64 @@ +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 + +@RestController +@RequestMapping("/v1/api-key") +class APIKeyController(private val apiKeyService: APIKeyServiceImpl) { + + @GetMapping + suspend fun getKeys(principal: Principal): List { + return apiKeyService.getKeysByUserId(principal.name) + .map { APIKeyResponse(it.label, it.expirationTime, it.allowedIPs, it.key, it.isEnabled) } + } + + @PostMapping + 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() + ) + 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) + } + + @PutMapping("/{key}/disable") + suspend fun disableKey(principal: Principal, @PathVariable key: String) { + apiKeyService.changeKeyState(principal.name, key, false) + } + + @DeleteMapping("/{key}") + suspend fun deleteKey(principal: Principal, @PathVariable key: String) { + apiKeyService.deleteKey(principal.name, key) + } + +} \ No newline at end of file diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyExpiration.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyExpiration.kt new file mode 100644 index 000000000..94e9f8650 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyExpiration.kt @@ -0,0 +1,22 @@ +package co.nilin.opex.api.app.data + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* +import java.util.concurrent.TimeUnit + +enum class APIKeyExpiration(private val unit: TimeUnit, private val duration: Long) { + + ONE_MONTH(TimeUnit.DAYS, 30), + THREE_MONTHS(TimeUnit.DAYS, 90), + SIX_MONTHS(TimeUnit.DAYS, 180), + ONE_YEAR(TimeUnit.DAYS, 365); + + private fun getDate() = Date(Date().time + unit.toMillis(duration)) + + fun getLocalDateTime(): LocalDateTime = with(Instant.ofEpochMilli(getDate().time)) { + LocalDateTime.ofInstant(this, ZoneId.systemDefault()) + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..dfe733f83 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/APIKeyResponse.kt @@ -0,0 +1,12 @@ +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 +) \ 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 new file mode 100644 index 000000000..e0846aa41 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/AccessTokenResponse.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.api.app.data + +data class AccessTokenResponse( + val access_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/CreateAPIKeyRequest.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt new file mode 100644 index 000000000..e84994737 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/data/CreateAPIKeyRequest.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.api.app.data + +data class CreateAPIKeyRequest( + val label: String, + val expiration: APIKeyExpiration?, + val allowedIPs: String? +) \ 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 new file mode 100644 index 000000000..f015a0ffd --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt @@ -0,0 +1,34 @@ +package co.nilin.opex.api.app.interceptor + +import co.nilin.opex.api.app.service.APIKeyServiceImpl +import co.nilin.opex.api.core.spi.APIKeyFilter +import kotlinx.coroutines.runBlocking +import org.springframework.stereotype.Component +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.WebFilter +import org.springframework.web.server.WebFilterChain +import reactor.core.publisher.Mono + +@Component +class APIKeyFilterImpl(private val apiKeyService: APIKeyServiceImpl) : APIKeyFilter, WebFilter { + + 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()) + } + } + return 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 new file mode 100644 index 000000000..5de6ae557 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/proxy/AuthProxy.kt @@ -0,0 +1,58 @@ +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.Value +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +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 logger = LoggerFactory.getLogger(AuthProxy::class.java) + + suspend fun exchangeToken(clientSecret: String, token: String): AccessTokenResponse { + val body = BodyInserters.fromFormData("client_id", "opex-api-key") + .with("client_secret", clientSecret) + .with("subject_token", token) + .with("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + .with("scope", "offline_access") + + logger.info("Request token exchange for user") + 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() + } + + suspend fun refreshToken(clientSecret: String, refreshToken: String): AccessTokenResponse { + val body = BodyInserters.fromFormData("client_id", "opex-api-key") + .with("client_secret", clientSecret) + .with("refresh_token", refreshToken) + .with("grant_type", "refresh_token") + + logger.info("Refreshing token") + 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/service/APIKeyServiceImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt new file mode 100644 index 000000000..7e23ba483 --- /dev/null +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt @@ -0,0 +1,209 @@ +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.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.awaitFirstOrElse +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 OpexException(OpexError.APIKeyLimitReached) + + 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)?.also { logger.info("Got apiKey from cache") } + ?: 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 OpexException(OpexError.NotFound) + if (apiKey.userId != userId) + throw OpexException(OpexError.Forbidden) + apiKey.isEnabled = isEnabled + apiKeyRepository.save(apiKey).awaitSingle() + } + + override suspend fun deleteKey(userId: String, key: String) { + val apiKey = apiKeyRepository.findByKey(key).awaitSingleOrNull() ?: throw OpexException(OpexError.NotFound) + if (apiKey.userId != userId) + throw OpexException(OpexError.Forbidden) + apiKeyRepository.delete(apiKey).awaitSingle() + } + + private suspend fun checkupAPIKey(apiKey: APIKeyModel, secret: String) { + if (apiKey.isExpired || !apiKey.isEnabled) + return + + logger.info("Checking up api key...") + 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) + logger.info("Added to cache") + } + } + + private fun updateCache(apiKey: APIKeyModel) { + getCache()?.apply { + evict(apiKey.key) + put(apiKey.key, apiKey) + logger.info("Cache updated") + } + } + + 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/resources/application.yml b/api/api-app/src/main/resources/application.yml index 929405bd4..e78bde202 100644 --- a/api/api-app/src/main/resources/application.yml +++ b/api/api-app/src/main/resources/application.yml @@ -57,6 +57,9 @@ app: url: lb://opex-bc-gateway auth: cert-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/certs + token-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/token + api-key-client: + secret: ${API_KEY_CLIENT_SECRET} binance: api-url: https://api1.binance.com swagger.authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/APIKey.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/APIKey.kt new file mode 100644 index 000000000..4868c4667 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/APIKey.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.api.core.inout + +import java.time.LocalDateTime + +data class APIKey( + val userId: String, + val label: String, + val accessToken: String?, + val expirationTime: LocalDateTime?, + val allowedIPs: String?, + val key: String, + val isEnabled: Boolean, + val isExpired: Boolean +) \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyFilter.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyFilter.kt new file mode 100644 index 000000000..523abccc8 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyFilter.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.api.core.spi + +interface APIKeyFilter \ No newline at end of file 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 new file mode 100644 index 000000000..f5329c6fa --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/APIKeyService.kt @@ -0,0 +1,24 @@ +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) + +} \ 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 e029af4e8..114008c6e 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 @@ -1,13 +1,17 @@ package co.nilin.opex.api.ports.binance.config +import co.nilin.opex.api.core.spi.APIKeyFilter +import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.server.WebFilter @EnableWebFluxSecurity class SecurityConfig(private val webClient: WebClient) { @@ -15,6 +19,9 @@ class SecurityConfig(private val webClient: WebClient) { @Value("\${app.auth.cert-url}") private lateinit var jwkUrl: String + @Autowired + private lateinit var apiKeyFilter: APIKeyFilter + @Bean fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? { http.csrf().disable() @@ -34,6 +41,7 @@ class SecurityConfig(private val webClient: WebClient) { .pathMatchers("/**").hasAuthority("SCOPE_trust") .anyExchange().authenticated() .and() + .addFilterBefore(apiKeyFilter as WebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .oauth2ResourceServer() .jwt() return http.build() diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/APIKeyRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/APIKeyRepository.kt new file mode 100644 index 000000000..21fa5d9fc --- /dev/null +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/APIKeyRepository.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.api.ports.postgres.dao + +import co.nilin.opex.api.ports.postgres.model.APIKeyModel +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +@Repository +interface APIKeyRepository : ReactiveCrudRepository { + + fun findAllByUserId(userId: String): Flux + + fun findByKey(key: String): Mono + + fun countByUserId(userId: String): Mono + +} \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt new file mode 100644 index 000000000..77f4bf4c5 --- /dev/null +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/APIKeyModel.kt @@ -0,0 +1,23 @@ +package co.nilin.opex.api.ports.postgres.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDateTime +import java.util.UUID + +@Table("api_key") +data class APIKeyModel( + @Id val id: Long? = null, + val userId: String, + val label: String, + var accessToken: String, + var refreshToken: String, + val expirationTime: LocalDateTime?, + @Column("allowed_ips") + val allowedIPs: String?, + var tokenExpirationTime: LocalDateTime, + val key: String = UUID.randomUUID().toString(), + var isEnabled: Boolean = true, + var isExpired: Boolean = false +) \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt index e72ed124a..68077129c 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/SymbolMapModel.kt @@ -1,6 +1,5 @@ package co.nilin.opex.api.ports.postgres.model - import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table diff --git a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql index cf27d5df0..9285767ce 100644 --- a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql +++ b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql @@ -6,3 +6,18 @@ CREATE TABLE IF NOT EXISTS symbol_maps alias VARCHAR(72) NOT NULL, UNIQUE (symbol, alias_key, alias) ); + +CREATE TABLE IF NOT EXISTS api_key +( + id SERIAL PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + label VARCHAR(200) NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT NOT NULL, + expiration_time TIMESTAMP NOT NULL, + allowed_ips TEXT, + token_expiration_time TIMESTAMP NOT NULL, + key VARCHAR(36) NOT NULL UNIQUE, + is_enabled BOOLEAN NOT NULL DEFAULT true, + is_expired BOOLEAN NOT NULL DEFAULT false +); diff --git a/docker-compose.yml b/docker-compose.yml index 5802590f3..4505e47a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -357,6 +357,7 @@ services: - VAULT_HOST=vault - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL - PREFERENCES=$PREFERENCES + - API_KEY_CLIENT_SECRET=$API_KEY_CLIENT_SECRET configs: - preferences.yml depends_on: diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index c5188b1e6..5cea6f33b 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -53,6 +53,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus InvalidPriceChangeDuration(7005, "Valid durations: [24h, 7d, 1m]", HttpStatus.BAD_REQUEST), CancelOrderNotAllowed(7006, "Canceling this order is not allowed", HttpStatus.FORBIDDEN), InvalidInterval(7007, "Invalid interval", HttpStatus.BAD_REQUEST), + APIKeyLimitReached(7007, "Reached API key limit. Maximum number of API key is 10", HttpStatus.BAD_REQUEST), // code 8000: bc-gateway ReservedAddressNotAvailable(8001, "No reserved address available", HttpStatus.BAD_REQUEST), From 24d02e18b72d0352749c2f25d52ce00880ff9c47 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sat, 29 Oct 2022 12:57:39 +0330 Subject: [PATCH 02/11] Modify opex-realm.json file --- .../src/main/resources/opex-realm.json | 216 ++++++++++++++++-- 1 file changed, 201 insertions(+), 15 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/resources/opex-realm.json b/user-management/keycloak-gateway/src/main/resources/opex-realm.json index fbd2d1cc9..aa525c414 100644 --- a/user-management/keycloak-gateway/src/main/resources/opex-realm.json +++ b/user-management/keycloak-gateway/src/main/resources/opex-realm.json @@ -56,15 +56,6 @@ "containerId": "opex", "attributes": {} }, - { - "id": "6061c17a-30fb-4d17-9414-8e20e61520ce", - "name": "admin_system", - "description": "Admins responsible for system's settings and operations", - "composite": false, - "clientRole": false, - "containerId": "opex", - "attributes": {} - }, { "id": "470642d4-8042-4eef-8146-cd8e5dc0c346", "name": "user_anonymous", @@ -125,6 +116,15 @@ "containerId": "opex", "attributes": {} }, + { + "id": "6061c17a-30fb-4d17-9414-8e20e61520ce", + "name": "admin_system", + "description": "Admins responsible for system's settings and operations", + "composite": false, + "clientRole": false, + "containerId": "opex", + "attributes": {} + }, { "id": "fee989a8-c92e-4889-9507-c37809d8f876", "name": "user_kyc", @@ -363,6 +363,7 @@ "attributes": {} } ], + "opex-api-key": [], "security-admin-console": [], "admin-cli": [], "account-console": [], @@ -671,6 +672,13 @@ "uma_authorization" ] }, + { + "client": "opex-api-key", + "roles": [ + "user_basic", + "user_kyc" + ] + }, { "client": "web-app", "roles": [ @@ -1285,6 +1293,65 @@ "microprofile-jwt" ] }, + { + "id": "d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "clientId": "opex-api-key", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "access.token.lifespan": "43200", + "saml.multivalued.roles": "false", + "saml.force.post.binding": "false", + "saml.encrypt": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "trust", + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, { "id": "6a4bfbd0-576d-4778-af56-56f876647355", "clientId": "realm-management", @@ -1494,6 +1561,37 @@ "name": "map-roles-composite" } ] + }, + { + "name": "client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "Client", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "753388dc-ff2f-48af-94b5-549c020d9493", + "uris": [], + "scopes": [ + { + "name": "view" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "configure" + }, + { + "name": "map-roles" + }, + { + "name": "manage" + }, + { + "name": "token-exchange" + }, + { + "name": "map-roles-composite" + } + ] } ], "policies": [ @@ -1507,6 +1605,16 @@ "clients": "[\"opex-admin\",\"account-console\"]" } }, + { + "id": "5edfc919-8dde-440b-86f4-802be17d55a4", + "name": "opex-api-exchange", + "type": "client", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "clients": "[\"web-app\"]" + } + }, { "id": "40160771-c030-4d2f-964d-886b6d574ba0", "name": "manage.permission.client.13d76feb-d762-4409-bb84-7a75bc395a61", @@ -1804,6 +1912,84 @@ "resources": "[\"client.resource.fb5f91c4-42fa-4769-b45d-febef22b4976\"]", "scopes": "[\"token-exchange\"]" } + }, + { + "id": "67c10cce-61d0-47ae-8d00-2f6da8ee67f9", + "name": "manage.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"manage\"]" + } + }, + { + "id": "19001b70-32e9-4582-ba10-b809d7a897c1", + "name": "configure.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"configure\"]" + } + }, + { + "id": "3cde330d-f57d-4e18-8c21-732733b70863", + "name": "view.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"view\"]" + } + }, + { + "id": "852537cc-a25f-454a-ba3d-b0bbc719ea9c", + "name": "map-roles.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"map-roles\"]" + } + }, + { + "id": "cc79158e-68ff-4de5-ab80-959a4e62b0d6", + "name": "map-roles-client-scope.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"map-roles-client-scope\"]" + } + }, + { + "id": "d6ba0206-d138-42a6-8ade-2b101181b791", + "name": "map-roles-composite.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"map-roles-composite\"]" + } + }, + { + "id": "ba5cbeab-c4f8-4103-bfc0-936b096abeb5", + "name": "token-exchange.permission.client.d2f0f1b6-46b7-4678-842a-8c67524ea2da", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.d2f0f1b6-46b7-4678-842a-8c67524ea2da\"]", + "scopes": "[\"token-exchange\"]", + "applyPolicies": "[\"opex-api-exchange\"]" + } } ], "scopes": [ @@ -2781,14 +2967,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-attribute-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", - "oidc-sha256-pairwise-sub-mapper" + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper" ] } } From d47ed9224a24c9d82545d013b0316a5da3eb9862 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 30 Oct 2022 16:15:03 +0330 Subject: [PATCH 03/11] Fix api key delete error --- .../kotlin/co/nilin/opex/api/app/service/APIKeyServiceImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 7e23ba483..93e1f42d2 100644 --- 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 @@ -10,6 +10,7 @@ import co.nilin.opex.utility.error.data.OpexException 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 @@ -121,7 +122,7 @@ class APIKeyServiceImpl( val apiKey = apiKeyRepository.findByKey(key).awaitSingleOrNull() ?: throw OpexException(OpexError.NotFound) if (apiKey.userId != userId) throw OpexException(OpexError.Forbidden) - apiKeyRepository.delete(apiKey).awaitSingle() + apiKeyRepository.delete(apiKey).awaitFirstOrNull() } private suspend fun checkupAPIKey(apiKey: APIKeyModel, secret: String) { From 0c2f46365d163d58cd1529079967545bf8ffe17d Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 14 Nov 2022 15:40:39 +0330 Subject: [PATCH 04/11] Add user level --- .../opex/accountant/app/config/AppConfig.kt | 4 +- .../app/config/InitializeService.kt | 9 +++- .../accountant/app/listener/OrderListener.kt | 3 +- .../core/service/OrderManagerImpl.kt | 11 ++++- .../accountant/core/spi/UserLevelLoader.kt | 7 +++ .../core/service/OrderManagerImplTest.kt | 3 ++ .../core/service/TradeManagerImplTest.kt | 9 ++-- .../listener/inout/OrderSubmitRequest.kt | 1 + .../postgres/dao/UserLevelMapperRepository.kt | 13 ++++++ .../ports/postgres/dao/UserLevelRepository.kt | 15 ++++++ .../postgres/impl/PairConfigLoaderImpl.kt | 5 +- .../postgres/impl/UserLevelLoaderImpl.kt | 15 ++++++ .../postgres/model/UserLevelMapperModel.kt | 11 +++++ .../ports/postgres/model/UserLevelModel.kt | 7 +++ .../src/main/resources/schema.sql | 19 ++++++-- .../opex/api/core/spi/MatchingGatewayProxy.kt | 1 + .../binance/controller/AccountController.kt | 4 +- .../ports/binance/util/SecurityExtension.kt | 7 +++ .../src/main/resources/schema.sql | 2 +- .../ports/proxy/data/CreateOrderRequest.kt | 3 +- .../proxy/impl/MatchingGatewayProxyImpl.kt | 4 +- .../core/eventh/events/SubmitOrderEvent.kt | 3 +- .../engine/core/inout/OrderSubmitRequest.kt | 46 +++++-------------- .../gateway/app/inout/CreateOrderRequest.kt | 3 +- .../gateway/app/service/OrderService.kt | 3 +- .../gateway/app/service/sample/Samples.kt | 3 +- .../submitter/inout/OrderSubmitRequest.kt | 1 + preferences-dev.yml | 3 ++ .../opex/utility/preferences/Preferences.kt | 1 + 29 files changed, 159 insertions(+), 57 deletions(-) create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt 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 e4fe77f13..85d3a389a 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 @@ -41,15 +41,17 @@ class AppConfig { @Bean fun orderManager( pairConfigLoader: PairConfigLoader, + userLevelLoader: UserLevelLoader, financialActionPersister: FinancialActionPersister, financeActionLoader: FinancialActionLoader, orderPersister: OrderPersister, tempEventPersister: TempEventPersister, tempEventRepublisher: TempEventRepublisher, - richOrderPublisher: RichOrderPublisher + richOrderPublisher: RichOrderPublisher, ): OrderManager { return OrderManagerImpl( pairConfigLoader, + userLevelLoader, financialActionPersister, financeActionLoader, orderPersister, 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 b78692c49..20d751529 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 @@ -2,6 +2,7 @@ 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 @@ -15,13 +16,19 @@ import javax.annotation.PostConstruct @DependsOn("postgresConfig") class InitializeService( private val pairConfigRepository: PairConfigRepository, - private val pairFeeConfigRepository: PairFeeConfigRepository + private val pairFeeConfigRepository: PairFeeConfigRepository, + private val userLevelRepository: UserLevelRepository, ) { + @Autowired private lateinit var preferences: Preferences @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 } diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt index 456cd1a79..52f95ef56 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt @@ -30,7 +30,8 @@ class OrderListener(private val orderManager: OrderManager) : OrderSubmitRequest event.quantity, event.direction, event.matchConstraint, - event.orderType + event.orderType, + event.userLevel ) ) } 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 b1ca47f66..9c840bf73 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 @@ -15,6 +15,7 @@ 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, @@ -32,7 +33,13 @@ open class OrderManagerImpl( } else { submitOrderEvent.pair.rightSideName } - val pairFeeConfig = pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "") + + val level = userLevelLoader.load(submitOrderEvent.uuid) + val pairFeeConfig = 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 @@ -70,7 +77,7 @@ open class OrderManagerImpl( pairFeeConfig.pairConfig.leftSideFraction, pairFeeConfig.pairConfig.rightSideFraction, submitOrderEvent.uuid, - "", + submitOrderEvent.userLevel, submitOrderEvent.direction, submitOrderEvent.matchConstraint, submitOrderEvent.orderType, 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 new file mode 100644 index 000000000..b11b250bb --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.accountant.core.spi + +interface UserLevelLoader { + + suspend fun load(uuid: String): String + +} \ 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 66193ff06..88b71c760 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 @@ -33,9 +33,11 @@ internal class OrderManagerImplTest { private val tempEventPersister = mockk() private val pairConfigLoader = mockk() private val richOrderPublisher = mockk() + private val userLevelLoader = mockk() private val orderManager = OrderManagerImpl( pairConfigLoader, + userLevelLoader, financialActionPersister, financialActionLoader, orderPersister, @@ -50,6 +52,7 @@ internal class OrderManagerImplTest { coEvery { tempEventPersister.saveTempEvent(any(), any()) } returns any() coEvery { financialActionLoader.findLast(any(), any()) } returns null coEvery { financialActionPersister.persist(any()) } returnsArgument (0) + coEvery { userLevelLoader.load(any()) } returns "*" } @Test 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 34de594b0..e5e7c6760 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 @@ -26,9 +26,11 @@ internal class TradeManagerImplTest { private val tempEventPersister = mockk() private val richOrderPublisher = mockk() private val richTradePublisher = mockk() + private val userLevelLoader = mockk() private val orderManager = OrderManagerImpl( pairConfigLoader, + userLevelLoader, financialActionPersister, financeActionLoader, orderPersister, @@ -48,10 +50,11 @@ internal class TradeManagerImplTest { init { coEvery { tempEventPersister.loadTempEvents(any()) } returns emptyList() - coEvery { orderPersister.save(any()) } returnsArgument(0) - coEvery { financeActionLoader.findLast(any(),any()) } returns null + coEvery { orderPersister.save(any()) } returnsArgument (0) + coEvery { financeActionLoader.findLast(any(), any()) } returns null coEvery { richOrderPublisher.publish(any()) } returns Unit coEvery { richTradePublisher.publish(any()) } returns Unit + coEvery { userLevelLoader.load(any()) } returns "*" } @Test @@ -210,7 +213,7 @@ internal class TradeManagerImplTest { orderPairFeeConfig.pairConfig.leftSideFraction, orderPairFeeConfig.pairConfig.rightSideFraction, submitOrderEvent.uuid, - "", + submitOrderEvent.userLevel, submitOrderEvent.direction, submitOrderEvent.matchConstraint, submitOrderEvent.orderType, diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt index 849158e22..6790172ae 100644 --- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt +++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt @@ -15,4 +15,5 @@ data class OrderSubmitRequest( val direction: OrderDirection, val matchConstraint: MatchConstraint, val orderType: OrderType, + 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/dao/UserLevelMapperRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt new file mode 100644 index 000000000..9156013b6 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000..a64ce628d --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt @@ -0,0 +1,15 @@ +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 { + + @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/impl/PairConfigLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt index 52c2c2ee2..8bef85947 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 @@ -5,6 +5,7 @@ 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.dao.UserLevelMapperRepository import co.nilin.opex.accountant.ports.postgres.model.PairConfigModel import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel import co.nilin.opex.matching.engine.core.model.OrderDirection @@ -19,8 +20,8 @@ import java.math.BigDecimal @Component class PairConfigLoaderImpl( - val pairConfigRepository: PairConfigRepository, - val pairFeeConfigRepository: PairFeeConfigRepository + private val pairConfigRepository: PairConfigRepository, + private val pairFeeConfigRepository: PairFeeConfigRepository ) : PairConfigLoader { override suspend fun loadPairConfigs(): List { 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 new file mode 100644 index 000000000..f18ca588b --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.spi.UserLevelLoader +import co.nilin.opex.accountant.ports.postgres.dao.UserLevelMapperRepository +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.springframework.stereotype.Component + +@Component +class UserLevelLoaderImpl(private val userLevelMapperRepository: UserLevelMapperRepository) : UserLevelLoader { + + override suspend fun load(uuid: String): String { + val mapper = userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull() + return mapper?.userLevel ?: "*" + } +} \ 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/UserLevelMapperModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt new file mode 100644 index 000000000..a8677d8dc --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt @@ -0,0 +1,11 @@ +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 new file mode 100644 index 000000000..2e9af91b0 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt @@ -0,0 +1,7 @@ +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/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql index 5b2303ce5..762694d1a 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 @@ -54,10 +54,12 @@ CREATE TABLE IF NOT EXISTS pair_config right_side_wallet_symbol VARCHAR(36) NOT NULL, left_side_fraction DECIMAL NOT NULL, right_side_fraction DECIMAL NOT NULL, - UNIQUE ( - left_side_wallet_symbol, - right_side_wallet_symbol - ) + UNIQUE (left_side_wallet_symbol, right_side_wallet_symbol) +); + +CREATE TABLE IF NOT EXISTS user_level +( + level VARCHAR(36) PRIMARY KEY ); CREATE TABLE IF NOT EXISTS pair_fee_config @@ -65,12 +67,19 @@ CREATE TABLE IF NOT EXISTS pair_fee_config id SERIAL PRIMARY KEY, pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), direction VARCHAR(36) NOT NULL, - user_level VARCHAR(36) NOT NULL, + 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) ); +CREATE TABLE IF NOT EXISTS user_level_mapper +( + id SERIAL PRIMARY KEY, + uuid VARCHAR(36) NOT NULL UNIQUE, + user_level VARCHAR(36) NOT NULL REFERENCES user_level (level) +); + CREATE TABLE IF NOT EXISTS temp_events ( id SERIAL PRIMARY KEY, 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 973ad9b58..906aec319 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 @@ -16,6 +16,7 @@ interface MatchingGatewayProxy { direction: OrderDirection, matchConstraint: MatchConstraint?, orderType: MatchingOrderType, + userLevel: String, token: String? ): OrderSubmitResult? diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt index f26f81bc6..4edfd83d2 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt @@ -98,7 +98,9 @@ class AccountController( quantity ?: BigDecimal.ZERO, side.asOrderDirection(), timeInForce?.asMatchConstraint(), - type.asMatchingOrderType(), securityContext.jwtAuthentication().tokenValue() + type.asMatchingOrderType(), + "*", + securityContext.jwtAuthentication().tokenValue() ) return NewOrderResponse( symbol, diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/SecurityExtension.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/SecurityExtension.kt index 3cd8a43ca..dc63da1e5 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/SecurityExtension.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/SecurityExtension.kt @@ -1,5 +1,6 @@ package co.nilin.opex.api.ports.binance.util +import com.nimbusds.jose.shaded.json.JSONArray import org.springframework.security.core.context.SecurityContext import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken @@ -9,4 +10,10 @@ fun SecurityContext.jwtAuthentication(): JwtAuthenticationToken { fun JwtAuthenticationToken.tokenValue(): String { return this.token.tokenValue +} + +fun JwtAuthenticationToken.roles(): List { + val list = arrayListOf() + (token.claims["roles"] as JSONArray?)?.forEach { list.add(it as String) } + return list } \ 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 index 9285767ce..1d4db9878 100644 --- a/api/api-ports/api-persister-postgres/src/main/resources/schema.sql +++ b/api/api-ports/api-persister-postgres/src/main/resources/schema.sql @@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS api_key label VARCHAR(200) NOT NULL, access_token TEXT NOT NULL, refresh_token TEXT NOT NULL, - expiration_time TIMESTAMP NOT NULL, + expiration_time TIMESTAMP, allowed_ips TEXT, token_expiration_time TIMESTAMP NOT NULL, key VARCHAR(36) NOT NULL UNIQUE, diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/CreateOrderRequest.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/CreateOrderRequest.kt index 4aabaccbd..4e523d96d 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/CreateOrderRequest.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/data/CreateOrderRequest.kt @@ -12,5 +12,6 @@ data class CreateOrderRequest( val quantity: BigDecimal, val direction: OrderDirection, val matchConstraint: MatchConstraint?, - val orderType: MatchingOrderType + val orderType: MatchingOrderType, + val userLevel: String ) \ No newline at end of file diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt index 85b186928..1f5a9b2ee 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/MatchingGatewayProxyImpl.kt @@ -35,15 +35,17 @@ class MatchingGatewayProxyImpl(private val client: WebClient) : MatchingGatewayP direction: OrderDirection, matchConstraint: MatchConstraint?, orderType: MatchingOrderType, + userLevel: String, token: String? ): OrderSubmitResult? { logger.info("calling matching-gateway order create") + val body = CreateOrderRequest(uuid, pair, price, quantity, direction, matchConstraint, orderType, userLevel) return client.post() .uri(URI.create("$baseUrl/order")) .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer $token") - .body(Mono.just(CreateOrderRequest(uuid, pair, price, quantity, direction, matchConstraint, orderType))) + .body(Mono.just(body)) .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) .bodyToMono() diff --git a/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/eventh/events/SubmitOrderEvent.kt b/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/eventh/events/SubmitOrderEvent.kt index 71b2e1a17..d7fa42f06 100644 --- a/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/eventh/events/SubmitOrderEvent.kt +++ b/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/eventh/events/SubmitOrderEvent.kt @@ -15,7 +15,8 @@ class SubmitOrderEvent( var remainedQuantity: Long = 0, var direction: OrderDirection = OrderDirection.ASK, var matchConstraint: MatchConstraint = MatchConstraint.GTC, - var orderType: OrderType = OrderType.LIMIT_ORDER + var orderType: OrderType = OrderType.LIMIT_ORDER, + val userLevel: String = "" ) : CoreEvent(pair), OneOrderEvent { override fun ouid(): String { diff --git a/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderSubmitRequest.kt b/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderSubmitRequest.kt index d1519366f..e8c0e2fb2 100644 --- a/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderSubmitRequest.kt +++ b/matching-engine/matching-engine-core/src/main/kotlin/co/nilin/opex/matching/engine/core/inout/OrderSubmitRequest.kt @@ -5,37 +5,15 @@ import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.matching.engine.core.model.OrderType import co.nilin.opex.matching.engine.core.model.Pair -class OrderSubmitRequest() { - - lateinit var ouid: String - lateinit var uuid: String - var orderId: Long? = null - lateinit var pair: Pair - var price: Long = 0 - var quantity: Long = 0 - var direction: OrderDirection = OrderDirection.BID - var matchConstraint: MatchConstraint = MatchConstraint.GTC - var orderType: OrderType = OrderType.LIMIT_ORDER - - constructor( - ouid: String, - uuid: String, - orderId: Long?, - pair: Pair, - price: Long, - quantity: Long, - direction: OrderDirection, - matchConstraint: MatchConstraint, - orderType: OrderType - ) : this() { - this.ouid = ouid - this.uuid = uuid - this.orderId = orderId - this.pair = pair - this.price = price - this.quantity = quantity - this.direction = direction - this.matchConstraint = matchConstraint - this.orderType = orderType - } -} \ No newline at end of file +class OrderSubmitRequest( + var ouid: String, + var uuid: String, + var pair: Pair, + var orderId: Long? = null, + var price: Long = 0, + var quantity: Long = 0, + var direction: OrderDirection = OrderDirection.BID, + var matchConstraint: MatchConstraint = MatchConstraint.GTC, + var orderType: OrderType = OrderType.LIMIT_ORDER, + var userLevel: String = "" +) \ No newline at end of file diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/CreateOrderRequest.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/CreateOrderRequest.kt index ddcb5f569..136b8a998 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/CreateOrderRequest.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/CreateOrderRequest.kt @@ -12,5 +12,6 @@ data class CreateOrderRequest( val quantity: BigDecimal, val direction: OrderDirection, val matchConstraint: MatchConstraint, - val orderType: OrderType + val orderType: OrderType, + val userLevel: String ) \ No newline at end of file diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt index 24672f38f..05b2f0fa5 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt @@ -66,7 +66,8 @@ class OrderService( .longValueExact(), createOrderRequest.direction, createOrderRequest.matchConstraint, - createOrderRequest.orderType + createOrderRequest.orderType, + createOrderRequest.userLevel ) return orderSubmitter.submit(orderSubmitRequest) } diff --git a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt index 03ca32c9d..db4aaba58 100644 --- a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt +++ b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/sample/Samples.kt @@ -30,7 +30,8 @@ object VALID { BigDecimal.valueOf(0.001), OrderDirection.ASK, MatchConstraint.GTC, - OrderType.LIMIT_ORDER + OrderType.LIMIT_ORDER, + "*" ) val CREATE_ORDER_REQUEST_BID = CREATE_ORDER_REQUEST_ASK.copy(direction = OrderDirection.BID) diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt index a295e5ba2..d6c5eb05e 100644 --- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt @@ -14,6 +14,7 @@ data class OrderSubmitRequest( val direction: OrderDirection, val matchConstraint: MatchConstraint, val orderType: OrderType, + val userLevel: String, val ouid: String = UUID.randomUUID().toString(), val orderId: Long? = null, ) \ No newline at end of file diff --git a/preferences-dev.yml b/preferences-dev.yml index b598baf63..d8d616c99 100644 --- a/preferences-dev.yml +++ b/preferences-dev.yml @@ -258,6 +258,9 @@ userLimits: dailyCount: 100 monthlyTotal: 30000 monthlyCount: 3000 +userLevels: + - "*" + - "nofee" system: walletTitle: system walletLevel: basic diff --git a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt index 543bd25d1..4da1bf3f7 100644 --- a/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt +++ b/utility/preferences/src/main/kotlin/co/nilin/opex/utility/preferences/Preferences.kt @@ -6,6 +6,7 @@ data class Preferences( var currencies: List = emptyList(), var markets: List = emptyList(), var userLimits: List = emptyList(), + var userLevels: List = emptyList(), var system: System = System(), val auth: Auth = Auth() ) From 9dc47e934416b9b7adee8c6cb0b717727383f3a1 Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 14 Nov 2022 16:09:34 +0330 Subject: [PATCH 05/11] Fix test failure --- .../opex/accountant/core/service/OrderManagerImplTest.kt | 4 ++-- .../opex/accountant/core/service/TradeManagerImplTest.kt | 2 +- preferences-demo.yml | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) 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 88b71c760..201fdc255 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 @@ -71,7 +71,7 @@ internal class OrderManagerImplTest { ) coEvery { - pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "") + pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any()) } returns PairFeeConfig( pairConfig, submitOrderEvent.direction.toString(), @@ -127,7 +127,7 @@ internal class OrderManagerImplTest { ) coEvery { - pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "") + pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any()) } returns PairFeeConfig( pairConfig, submitOrderEvent.direction.toString(), 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 e5e7c6760..4e577b3cb 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 @@ -187,7 +187,7 @@ internal class TradeManagerImplTest { takerFee: BigDecimal ) { coEvery { - pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "") + pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any()) } returns PairFeeConfig( pairConfig, submitOrderEvent.direction.toString(), diff --git a/preferences-demo.yml b/preferences-demo.yml index b57dd1784..ba6c25320 100644 --- a/preferences-demo.yml +++ b/preferences-demo.yml @@ -225,6 +225,9 @@ userLimits: system: walletTitle: system walletLevel: basic +userLevels: + - "*" + - "nofee" auth: whitelist: enabled: false From 31ef872e63be31ee5c09ab5129b8f2b1b00c3c91 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sat, 19 Nov 2022 15:23:26 +0330 Subject: [PATCH 06/11] Increase request size --- bc-gateway/bc-gateway-app/src/main/resources/application.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bc-gateway/bc-gateway-app/src/main/resources/application.yml b/bc-gateway/bc-gateway-app/src/main/resources/application.yml index 6b71ce2eb..52bb87857 100644 --- a/bc-gateway/bc-gateway-app/src/main/resources/application.yml +++ b/bc-gateway/bc-gateway-app/src/main/resources/application.yml @@ -40,11 +40,14 @@ spring: prefer-ip-address: true config: import: vault://secret/${spring.application.name} + codec: + max-in-memory-size: 20MB logging: level: org.apache.kafka: ERROR co.nilin: DEBUG -swagger.authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token +swagger: + authUrl: ${SWAGGER_AUTH_URL:https://api.opex.dev/auth}/realms/opex/protocol/openid-connect/token app: auth: cert-url: lb://opex-auth/auth/realms/opex/protocol/openid-connect/certs From 069a287cb8e5fb367fa07c4fa06e26c0646cfbf5 Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 21 Nov 2022 11:34:21 +0330 Subject: [PATCH 07/11] Fix bc-gateway async error --- .../core/service/WalletSyncServiceImpl.kt | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt index 01ff7d969..a33db4b2f 100644 --- a/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt +++ b/bc-gateway/bc-gateway-core/src/main/kotlin/co/nilin/opex/bcgateway/core/service/WalletSyncServiceImpl.kt @@ -29,20 +29,16 @@ class WalletSyncServiceImpl( @Transactional override suspend fun syncTransfers(transfers: List) = coroutineScope { val groupedByChain = currencyHandler.fetchAllImplementations().groupBy { it.chain.name } - val deposits = transfers.map { - async { - coroutineScope { - val currencyImpl = groupedByChain[it.chain]?.find { c -> c.tokenAddress == it.tokenAddress } - ?: throw IllegalStateException("Currency implementation not found") - assignedAddressHandler.findUuid(it.receiver.address, it.receiver.memo)?.let { it to currencyImpl } - }?.let { (uuid, currencyImpl) -> - sendDeposit(uuid, currencyImpl, it) - logger.info("Deposit synced for $uuid on ${currencyImpl.currency.symbol} - to ${it.receiver.address}") - it - } + val deposits = transfers.mapNotNull { + coroutineScope { + val currencyImpl = groupedByChain[it.chain]?.find { c -> c.tokenAddress == it.tokenAddress } + ?: throw IllegalStateException("Currency implementation not found") + assignedAddressHandler.findUuid(it.receiver.address, it.receiver.memo)?.let { it to currencyImpl } + }?.let { (uuid, currencyImpl) -> + sendDeposit(uuid, currencyImpl, it) + logger.info("Deposit synced for $uuid on ${currencyImpl.currency.symbol} - to ${it.receiver.address}") + it } - }.mapNotNull { - it.await() }.map { Deposit( null, From 2e1cc5552b1e2b4cb5d8bf5663b00925b612232f Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 23 Nov 2022 12:15:51 +0330 Subject: [PATCH 08/11] Fix log running queries --- .../ports/postgres/dao/TradeRepository.kt | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt index e3e107a78..cf6d619d8 100644 --- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt +++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt @@ -283,21 +283,23 @@ interface TradeRepository : ReactiveCrudRepository { @Query( """ + with first_trade as (select matched_price, symbol from trades where create_date > :since order by create_date limit 1), + last_trade as (select matched_price, symbol from trades where create_date > :since order by create_date desc limit 1) select symbol, - coalesce((select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1), 0.0) as last_price, + coalesce((select matched_price from last_trade where symbol = t.symbol), 0.0) as last_price, coalesce( max( - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1) - - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) + (select matched_price from last_trade where symbol = t.symbol) + - (select matched_price from first_trade where symbol = t.symbol) ), 0.0 ) as price_change, coalesce( ( - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1) - - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) - ) / (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) * 100, + (select matched_price from last_trade where symbol = t.symbol) + -(select matched_price from first_trade where symbol = t.symbol) + ) / (select matched_price from first_trade where symbol = t.symbol) * 100, 0.0 ) as price_change_percent from trades t @@ -310,21 +312,23 @@ interface TradeRepository : ReactiveCrudRepository { @Query( """ + with first_trade as (select matched_price, symbol from trades where create_date > :since order by create_date limit 1), + last_trade as (select matched_price, symbol from trades where create_date > :since order by create_date desc limit 1) select symbol, - coalesce((select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1), 0.0) as last_price, + coalesce((select matched_price from last_trade where symbol = t.symbol), 0.0) as last_price, coalesce( max( - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1) - - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) + (select matched_price from last_trade where symbol = t.symbol) + - (select matched_price from first_trade where symbol = t.symbol) ), 0.0 ) as price_change, coalesce( ( - (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1) - -(select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) - ) / (select matched_price from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) * 100, + (select matched_price from last_trade where symbol = t.symbol) + -(select matched_price from first_trade where symbol = t.symbol) + ) / (select matched_price from first_trade where symbol = t.symbol) * 100, 0.0 ) as price_change_percent from trades t @@ -337,15 +341,17 @@ interface TradeRepository : ReactiveCrudRepository { @Query( """ + with first_trade as (select matched_quantity as mq, symbol from trades where create_date > :since order by create_date limit 1), + last_trade as (select matched_quantity as mq, symbol from trades where create_date > :since order by create_date desc limit 1) select symbol, coalesce(sum(matched_quantity), 0.0) as volume, count(id) as trade_count, coalesce( ( - (select matched_quantity from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1) - - (select matched_quantity from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) - ) / (select matched_quantity from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) * 100, + (select mq from last_trade where symbol = t.symbol) + - (select mq from first_trade where symbol = t.symbol) + ) / (select mq from first_trade where symbol = t.symbol) * 100, 0.0 ) as change from trades t @@ -359,15 +365,17 @@ interface TradeRepository : ReactiveCrudRepository { @Query( """ + with first_trade as (select matched_quantity as mq, symbol from trades where create_date > :since order by create_date limit 1), + last_trade as (select matched_quantity as mq, symbol from trades where create_date > :since order by create_date desc limit 1) select symbol, coalesce(sum(matched_quantity), 0.0) as volume, count(id) as trade_count, coalesce( ( - (select matched_quantity from trades where create_date > :since and symbol = t.symbol order by create_date desc limit 1) - - (select matched_quantity from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) - ) / (select matched_quantity from trades where create_date > :since and symbol = t.symbol order by create_date limit 1) * 100, + (select mq from last_trade where symbol = t.symbol) + - (select mq from first_trade where symbol = t.symbol) + ) / (select mq from first_trade where symbol = t.symbol) * 100, 0.0 ) as change from trades t From 3f74fd0ad51493b234acf7068ac9ce43ead692a5 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 29 Nov 2022 16:14:01 +0330 Subject: [PATCH 09/11] Improve trade queries --- .../ports/postgres/dao/TradeRepository.kt | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt index cf6d619d8..8508f5335 100644 --- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt +++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/TradeRepository.kt @@ -46,7 +46,7 @@ interface TradeRepository : ReactiveCrudRepository { startTime: Date?, @Param("endTime") endTime: Date?, - limit:Int + limit: Int ): Flow @Query("select * from trades where symbol = :symbol order by create_date desc limit :limit") @@ -59,12 +59,14 @@ interface TradeRepository : ReactiveCrudRepository { @Query( """ + with first_trade as (select * from trades where create_date > :date order by create_date limit 1), + last_trade as (select * from trades where create_date > :date order by create_date desc limit 1) select symbol, - (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date limit 1) as price_change, - ((((select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date limit 1))/(select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date limit 1))*100) as price_change_percent, - (sum(matched_quantity)/sum(taker_price)) as weighted_avg_price, - (select taker_price from trades where create_date > :date and symbol=t.symbol order by create_date limit 1) as last_price, - (select matched_quantity from trades where create_date > :date and symbol=t.symbol order by create_date limit 1) as last_qty, + (select matched_price from last_trade where symbol=t.symbol) - (select matched_price from first_trade where symbol=t.symbol) as price_change, + ((((select matched_price from last_trade where symbol=t.symbol) - (select matched_price from first_trade where symbol=t.symbol))/(select matched_price from first_trade where symbol=t.symbol))*100) as price_change_percent, + (sum(matched_quantity)/sum(matched_price)) as weighted_avg_price, + (select matched_price from last_trade where symbol=t.symbol) as last_price, + (select matched_quantity from last_trade where symbol=t.symbol) as last_qty, ( select price from orders join order_status os on orders.ouid = os.ouid @@ -89,11 +91,11 @@ interface TradeRepository : ReactiveCrudRepository { and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) order by create_date desc limit 1 ) as open_price, - max(taker_price) as high_price, - min(taker_price) as low_price, + max(matched_price) as high_price, + min(matched_price) as low_price, sum(matched_quantity) as volume, - (select id from trades where create_date > :date and symbol=t.symbol order by create_date limit 1) as first_id, - (select id from trades where create_date > :date and symbol=t.symbol order by create_date desc limit 1) as last_id, + (select id from first_trade where symbol=t.symbol) as first_id, + (select id from last_trade where symbol=t.symbol) as last_id, count(id) as count from trades as t where create_date > :date @@ -104,12 +106,14 @@ interface TradeRepository : ReactiveCrudRepository { @Query( """ + with first_trade as (select * from trades where create_date > :date and symbol = :symbol order by create_date limit 1), + last_trade as (select * from trades where create_date > :date and symbol = :symbol order by create_date desc limit 1) select symbol, - (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date limit 1) as price_change, - ((((select taker_price from trades where create_date > :date and symbol=:symbol order by create_date desc limit 1) - (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date limit 1))/(select taker_price from trades where create_date > :date and symbol=:symbol order by create_date limit 1))*100) as price_change_percent, - (sum(matched_quantity)/sum(taker_price)) as weighted_avg_price, - (select taker_price from trades where create_date > :date and symbol=:symbol order by create_date limit 1) as last_price, - (select matched_quantity from trades where create_date > :date and symbol=:symbol order by create_date limit 1) as last_qty, + (select matched_price from last_trade) - (select matched_price from first_trade) as price_change, + ((((select matched_price from last_trade) - (select matched_price from first_trade))/(select matched_price from first_trade))*100) as price_change_percent, + (sum(matched_quantity)/sum(matched_price)) as weighted_avg_price, + (select matched_price from last_trade) as last_price, + (select matched_quantity from last_trade) as last_qty, ( select price from orders join order_status os on orders.ouid = os.ouid @@ -134,11 +138,11 @@ interface TradeRepository : ReactiveCrudRepository { and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) order by create_date desc limit 1 ) as open_price, - max(taker_price) as high_price, - min(taker_price) as low_price, + max(matched_price) as high_price, + min(matched_price) as low_price, sum(matched_quantity) as volume, - (select id from trades where create_date > :date and symbol=:symbol order by create_date limit 1) as first_id, - (select id from trades where create_date > :date and symbol=:symbol order by create_date desc limit 1) as last_id, + (select id from first_trade) as first_id, + (select id from last_trade) as last_id, count(id) as count from trades as t where create_date > :date and symbol = :symbol @@ -241,10 +245,10 @@ interface TradeRepository : ReactiveCrudRepository { select f.start_time as open_time, f.end_time as close_time, - (select taker_price from trades tt where symbol = :symbol and tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date limit 1) as open, - max(t.taker_price) as high, - min(t.taker_price) as low, - (select taker_price from trades tt where symbol = :symbol and tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date desc limit 1) as close, + (select matched_price from trades tt where symbol = :symbol and tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date limit 1) as open, + max(t.matched_price) as high, + min(t.matched_price) as low, + (select matched_price from trades tt where symbol = :symbol and tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date desc limit 1) as close, sum(t.matched_quantity) as volume, count(id) as trades from trades t @@ -252,7 +256,7 @@ interface TradeRepository : ReactiveCrudRepository { on t.create_date >= f.start_time and t.create_date < f.end_time where symbol = :symbol or symbol is null group by f.start_time, f.end_time - order by f.start_time asc + order by f.start_time limit :limit """ ) From 2615b58175d16021a66e42fafefe209a8d70d365 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 20 Dec 2022 12:00:08 +0330 Subject: [PATCH 10/11] * Update spring boot, vault, and cloud * Refactor transfer service * Add flatten function * Add batch transfer service * Close #336 --- .../accountant/core/inout/TransferRequest.kt | 14 ++ .../service/FinancialActionJobManagerImpl.kt | 49 ++++-- .../core/spi/FinancialActionPersister.kt | 2 + .../opex/accountant/core/spi/WalletProxy.kt | 3 + .../core/service/FAJobManagerImplTest.kt | 98 +++--------- .../accountant-persister-postgres/pom.xml | 2 +- .../postgres/dao/FinancialActionRepository.kt | 3 + .../impl/FinancialActionPersisterImpl.kt | 4 + .../accountant-wallet-proxy/pom.xml | 2 +- .../walletproxy/proxy/WalletProxyImpl.kt | 19 ++- api/api-ports/api-binance-rest/pom.xml | 2 +- api/api-ports/api-persister-postgres/pom.xml | 2 +- .../bc-gateway-persister-postgres/pom.xml | 2 +- docker-images/vault/Dockerfile | 2 +- .../eventlog-persister-postgres/pom.xml | 2 +- .../market-persister-postgres/pom.xml | 2 +- pom.xml | 4 +- .../referral-persister-postgres/pom.xml | 2 +- .../referral-wallet-proxy/pom.xml | 2 +- .../controller/PaymentGatewayController.kt | 6 +- .../app/controller/TransferController.kt | 93 +++-------- .../opex/wallet/app/dto/TransferRequest.kt | 14 ++ .../wallet/app/service/TransferService.kt | 142 +++++++++++++++++ .../opex/wallet/core/inout/WalletType.kt | 5 + ...nsferService.kt => TransferManagerImpl.kt} | 18 ++- .../wallet/core/service/WithdrawService.kt | 23 ++- .../opex/wallet/core/spi/TransferManager.kt | 10 ++ ...viceTest.kt => TransferManagerImplTest.kt} | 18 +-- .../wallet-persister-postgres/pom.xml | 2 +- .../postgres/impl/WalletOwnerManagerImpl.kt | 147 ++++++++---------- .../websocket-persister-postgres/pom.xml | 2 +- 31 files changed, 397 insertions(+), 299 deletions(-) create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/TransferRequest.kt create mode 100644 wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/TransferRequest.kt create mode 100644 wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt create mode 100644 wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WalletType.kt rename wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/{TransferService.kt => TransferManagerImpl.kt} (89%) create mode 100644 wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TransferManager.kt rename wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/{TransferServiceTest.kt => TransferManagerImplTest.kt} (93%) diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/TransferRequest.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/TransferRequest.kt new file mode 100644 index 000000000..5203b756f --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/TransferRequest.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.accountant.core.inout + +import java.math.BigDecimal + +data class TransferRequest( + val amount: BigDecimal, + val symbol: String, + val senderUuid: String, + val senderWalletType: String, + val receiverUuid: String, + val receiverWalletType: String, + val transferRef: String?, + val description: String? +) \ 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 cee6db269..944bb4a22 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 @@ -1,6 +1,8 @@ package co.nilin.opex.accountant.core.service import co.nilin.opex.accountant.core.api.FinancialActionJobManager +import co.nilin.opex.accountant.core.inout.TransferRequest +import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.FinancialActionStatus import co.nilin.opex.accountant.core.spi.FinancialActionLoader import co.nilin.opex.accountant.core.spi.FinancialActionPersister @@ -13,31 +15,44 @@ class FinancialActionJobManagerImpl( private val walletProxy: WalletProxy ) : FinancialActionJobManager { - private val retryLimit = 10 - private val log = LoggerFactory.getLogger(FinancialActionJobManagerImpl::class.java) + private val logger = LoggerFactory.getLogger(FinancialActionJobManagerImpl::class.java) override suspend fun processFinancialActions(offset: Long, size: Long) { val factions = financialActionLoader.loadUnprocessed(offset, size) - factions.forEach { - try { - walletProxy.transfer( + val flatten = sortAndFlattenFA(factions) + logger.info("Loaded ${flatten.size} factions: ${flatten.map { it.id }}") + if (factions.isEmpty()) + return + + try { + val requests = factions.map { + TransferRequest( + it.amount, it.symbol, - it.senderWalletType, it.sender, - it.receiverWalletType, + it.senderWalletType, it.receiver, - it.amount, - it.eventType + it.pointer, - null - ) - financialActionPersister.updateStatus(it, FinancialActionStatus.PROCESSED) - } catch (e: Exception) { - log.error("financial job error", e) - financialActionPersister.updateStatus( - it, - if (it.retryCount >= retryLimit) FinancialActionStatus.ERROR else FinancialActionStatus.CREATED + it.receiverWalletType, + null, + it.eventType + it.pointer ) } + walletProxy.batchTransfer(requests) + financialActionPersister.updateBatchStatus(factions, FinancialActionStatus.PROCESSED) + } catch (e: Exception) { + logger.error("financial job error", e) + } + } + + fun sortAndFlattenFA(list: List): Collection { + val result = arrayListOf() + + fun extractParent(fa: FinancialAction) { + if (fa.parent != null) + extractParent(fa.parent) + result.add(fa) } + list.forEach { extractParent(it) } + return result.distinctBy { it.id } } } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt index 53d0db4ff..6b66dc4c3 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt @@ -8,4 +8,6 @@ interface FinancialActionPersister { suspend fun persist(financialActions: List): List suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) + + suspend fun updateBatchStatus(financialAction: List, status: FinancialActionStatus) } \ 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 ed27996ca..a5ab84a65 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,6 @@ package co.nilin.opex.accountant.core.spi +import co.nilin.opex.accountant.core.inout.TransferRequest import java.math.BigDecimal interface WalletProxy { @@ -15,5 +16,7 @@ interface WalletProxy { transferRef: String? ) + suspend fun batchTransfer(transfers: List) + suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean } \ No newline at end of file diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FAJobManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FAJobManagerImplTest.kt index 3318eac7d..d2ba203c6 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FAJobManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FAJobManagerImplTest.kt @@ -1,6 +1,6 @@ package co.nilin.opex.accountant.core.service -import co.nilin.opex.accountant.core.model.FinancialActionStatus +import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.spi.FinancialActionLoader import co.nilin.opex.accountant.core.spi.FinancialActionPersister import co.nilin.opex.accountant.core.spi.WalletProxy @@ -9,6 +9,9 @@ import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDateTime +import org.assertj.core.api.Assertions.assertThat class FAJobManagerImplTest { @@ -20,93 +23,36 @@ class FAJobManagerImplTest { init { coEvery { financialActionLoader.loadUnprocessed(any(), any()) } returns listOf(Valid.fa, Valid.fa) - coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit - } - - @Test - fun given2FALoaded_whenProcessing_thenVerifyThatTransferProxyCalled2Times() = runBlocking { - coEvery { walletProxy.transfer(any(), any(), any(), any(), any(), any(), any(), any()) } returns Unit - sut.processFinancialActions(0, 2) - with(Valid.fa) { - coVerify(exactly = 2) { - walletProxy.transfer( - eq(symbol), - eq(senderWalletType), - eq(sender), - eq(receiverWalletType), - eq(receiver), - eq(amount), - eq(eventType + pointer), - any() - ) - } - coVerify(exactly = 2) { - financialActionPersister.updateStatus( - eq(this@with), - eq(FinancialActionStatus.PROCESSED) - ) - } - } + coEvery { financialActionPersister.updateBatchStatus(any(), any()) } returns Unit } @Test fun given2FALoaded_whenProcessingFailed_thenUpdateStatusCalledRegardless() = runBlocking { coEvery { - walletProxy.transfer(any(), any(), any(), any(), any(), any(), any(), any()) + walletProxy.batchTransfer(any()) } throws IllegalStateException() sut.processFinancialActions(0, 2) - with(Valid.fa) { - coVerify(exactly = 2) { - walletProxy.transfer( - eq(symbol), - eq(senderWalletType), - eq(sender), - eq(receiverWalletType), - eq(receiver), - eq(amount), - eq(eventType + pointer), - any() - ) - } - coVerify(exactly = 2) { - financialActionPersister.updateStatus( - eq(this@with), - eq(FinancialActionStatus.CREATED) - ) - } + coVerify(exactly = 1) { + walletProxy.batchTransfer(any()) } } @Test - fun given2FALoaded_whenProcessingFailedAndRetryCountExceeded_thenUpdateStatusCalledRegardless() = runBlocking { - coEvery { - walletProxy.transfer(any(), any(), any(), any(), any(), any(), any(), any()) - } throws IllegalStateException() - - coEvery { financialActionLoader.loadUnprocessed(any(), any()) } returns listOf(Valid.faHighRetry) - - sut.processFinancialActions(0, 1) - with(Valid.faHighRetry) { - coVerify(exactly = 1) { - walletProxy.transfer( - eq(symbol), - eq(senderWalletType), - eq(sender), - eq(receiverWalletType), - eq(receiver), - eq(amount), - eq(eventType + pointer), - any() - ) - } - coVerify(exactly = 1) { - financialActionPersister.updateStatus( - eq(this@with), - eq(FinancialActionStatus.ERROR) - ) - } - } + fun givenFALoaded_validateParentsAreFirstInLine(): Unit = runBlocking { + val fa1 = FinancialAction(null, "", "", "", BigDecimal.ZERO, "", "", "", "", LocalDateTime.now(), id = 1) + val fa2 = FinancialAction(fa1, "", "", "", BigDecimal.ZERO, "", "", "", "", LocalDateTime.now(), id = 2) + val fa3 = FinancialAction(fa1, "", "", "", BigDecimal.ZERO, "", "", "", "", LocalDateTime.now(), id = 3) + val fa4 = FinancialAction(fa3, "", "", "", BigDecimal.ZERO, "", "", "", "", LocalDateTime.now(), id = 4) + val fa5 = FinancialAction(null, "", "", "", BigDecimal.ZERO, "", "", "", "", LocalDateTime.now(), id = 5) + val list = arrayListOf(fa5, fa4, fa3, fa2, fa1) + + val flatten = sut.sortAndFlattenFA(list) + + assertThat(flatten.indexOf(fa1)).isLessThan(flatten.indexOf(fa2)) + assertThat(flatten.indexOf(fa1)).isLessThan(flatten.indexOf(fa3)) + assertThat(flatten.indexOf(fa1)).isLessThan(flatten.indexOf(fa4)) + assertThat(flatten.indexOf(fa3)).isLessThan(flatten.indexOf(fa4)) } } \ 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 331727e89..8d0ea453d 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/pom.xml +++ b/accountant/accountant-ports/accountant-persister-postgres/pom.xml @@ -33,7 +33,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime 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 1090e0859..4ef86a1fa 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 @@ -37,4 +37,7 @@ interface FinancialActionRepository : ReactiveCrudRepository + + @Query("update fi_actions set status = :status where id in (:ids)") + fun updateStatus(ids: List, status: FinancialActionStatus): 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/FinancialActionPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt index fb82e954d..7cb5bb08a 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 @@ -37,4 +37,8 @@ class FinancialActionPersisterImpl(private val financialActionRepository: Financ override suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) { financialActionRepository.updateStatusAndIncreaseRetry(financialAction.id!!, status).awaitFirstOrNull() } + + override suspend fun updateBatchStatus(financialAction: List, status: FinancialActionStatus) { + financialActionRepository.updateStatus(financialAction.mapNotNull { it.id }, status).awaitFirstOrNull() + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml index 7c3460d87..bb691dafd 100644 --- a/accountant/accountant-ports/accountant-wallet-proxy/pom.xml +++ b/accountant/accountant-ports/accountant-wallet-proxy/pom.xml @@ -45,7 +45,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime 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 70500e611..80ff777d0 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,13 +1,18 @@ package co.nilin.opex.accountant.ports.walletproxy.proxy +import co.nilin.opex.accountant.core.inout.TransferRequest 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 import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.body import org.springframework.web.reactive.function.client.bodyToMono +import reactor.core.publisher.Mono import java.math.BigDecimal @Component @@ -28,7 +33,7 @@ class WalletProxyImpl( ) { webClient.post() .uri("$walletBaseUrl/transfer/${amount}_$symbol/from/${senderUuid}_$senderWalletType/to/${receiverUuid}_$receiverWalletType") - .header("Content-Type", "application/json") + .contentType(MediaType.APPLICATION_JSON) .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) .bodyToMono() @@ -36,6 +41,18 @@ class WalletProxyImpl( .awaitFirst() } + override suspend fun batchTransfer(transfers: List) { + webClient.post() + .uri("$walletBaseUrl/transfer/batch") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(transfers)) + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono() + .log() + .awaitFirstOrNull() + } + override suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean { return webClient.get() .uri("$walletBaseUrl/$uuid/wallet_type/$walletType/can_withdraw/${amount}_$symbol") diff --git a/api/api-ports/api-binance-rest/pom.xml b/api/api-ports/api-binance-rest/pom.xml index 66ddd73a5..742dcd9ee 100644 --- a/api/api-ports/api-binance-rest/pom.xml +++ b/api/api-ports/api-binance-rest/pom.xml @@ -53,7 +53,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/api/api-ports/api-persister-postgres/pom.xml b/api/api-ports/api-persister-postgres/pom.xml index ee76e7b1a..83c6cd821 100644 --- a/api/api-ports/api-persister-postgres/pom.xml +++ b/api/api-ports/api-persister-postgres/pom.xml @@ -33,7 +33,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml index 7de49a35a..d5bda18ac 100644 --- a/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml +++ b/bc-gateway/bc-gateway-ports/bc-gateway-persister-postgres/pom.xml @@ -25,7 +25,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/docker-images/vault/Dockerfile b/docker-images/vault/Dockerfile index c252d77bb..a4e183276 100644 --- a/docker-images/vault/Dockerfile +++ b/docker-images/vault/Dockerfile @@ -1,4 +1,4 @@ -FROM vault:1.10.1 +FROM vault:1.12.2 COPY ["backend-policy.hcl", "panel-policy.hcl", "vault.json", "workflow-vault.sh", "/vault/config/"] EXPOSE 8200 ENTRYPOINT /vault/config/workflow-vault.sh diff --git a/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml b/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml index c65e3647d..be2b16d8f 100644 --- a/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml +++ b/eventlog/eventlog-ports/eventlog-persister-postgres/pom.xml @@ -33,7 +33,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/market/market-ports/market-persister-postgres/pom.xml b/market/market-ports/market-persister-postgres/pom.xml index df22b6931..62f06eade 100644 --- a/market/market-ports/market-persister-postgres/pom.xml +++ b/market/market-ports/market-persister-postgres/pom.xml @@ -33,7 +33,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/pom.xml b/pom.xml index 4db68166a..ec0f9f998 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ 11 11 1.6.0 - 2.6.2 - 2021.0.0 + 2.7.6 + 2021.0.5 true diff --git a/referral/referral-ports/referral-persister-postgres/pom.xml b/referral/referral-ports/referral-persister-postgres/pom.xml index bdb72669d..b16283529 100644 --- a/referral/referral-ports/referral-persister-postgres/pom.xml +++ b/referral/referral-ports/referral-persister-postgres/pom.xml @@ -25,7 +25,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/referral/referral-ports/referral-wallet-proxy/pom.xml b/referral/referral-ports/referral-wallet-proxy/pom.xml index 31c367031..f634dd19c 100644 --- a/referral/referral-ports/referral-wallet-proxy/pom.xml +++ b/referral/referral-ports/referral-wallet-proxy/pom.xml @@ -25,7 +25,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime 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 667bf8c59..5d4479ae3 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 @@ -7,8 +7,8 @@ import co.nilin.opex.wallet.app.dto.PaymentDepositRequest import co.nilin.opex.wallet.app.dto.PaymentDepositResponse import co.nilin.opex.wallet.core.inout.TransferCommand import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.service.TransferService import co.nilin.opex.wallet.core.spi.CurrencyService +import co.nilin.opex.wallet.core.spi.TransferManager import co.nilin.opex.wallet.core.spi.WalletManager import co.nilin.opex.wallet.core.spi.WalletOwnerManager import org.springframework.web.bind.annotation.PostMapping @@ -20,7 +20,7 @@ import java.math.BigDecimal @RestController @RequestMapping("/payment") class PaymentGatewayController( - val transferService: TransferService, + val transferManager: TransferManager, val currencyService: CurrencyService, val walletManager: WalletManager, val walletOwnerManager: WalletOwnerManager @@ -55,7 +55,7 @@ class PaymentGatewayController( receiverWalletType ) - val command = transferService.transfer( + val command = transferManager.transfer( TransferCommand( sourceWallet, receiverWallet, diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TransferController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TransferController.kt index 1cce26772..9006d0e07 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TransferController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/TransferController.kt @@ -1,30 +1,17 @@ package co.nilin.opex.wallet.app.controller -import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.OpexException -import co.nilin.opex.wallet.core.inout.TransferCommand +import co.nilin.opex.wallet.app.dto.TransferRequest +import co.nilin.opex.wallet.app.service.TransferService import co.nilin.opex.wallet.core.inout.TransferResult -import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.service.TransferService -import co.nilin.opex.wallet.core.spi.CurrencyService -import co.nilin.opex.wallet.core.spi.WalletManager -import co.nilin.opex.wallet.core.spi.WalletOwnerManager import io.swagger.annotations.ApiResponse import io.swagger.annotations.Example import io.swagger.annotations.ExampleProperty -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.math.BigDecimal @RestController -class TransferController( - val transferService: TransferService, - val currencyService: CurrencyService, - val walletManager: WalletManager, - val walletOwnerManager: WalletOwnerManager -) { +class TransferController(private val transferService: TransferService) { + @PostMapping("/transfer/{amount}_{symbol}/from/{senderUuid}_{senderWalletType}/to/{receiverUuid}_{receiverWalletType}") @ApiResponse( message = "OK", @@ -46,35 +33,21 @@ class TransferController( @RequestParam("description") description: String?, @RequestParam("transferRef") transferRef: String? ): TransferResult { - if (senderWalletType == "cashout" || receiverWalletType == "cashout") - throw OpexException(OpexError.InvalidCashOutUsage) - val currency = currencyService.getCurrency(symbol) ?: throw OpexException(OpexError.CurrencyNotFound) - val sourceOwner = walletOwnerManager.findWalletOwner(senderUuid) - ?: throw OpexException(OpexError.WalletOwnerNotFound) - val sourceWallet = walletManager.findWalletByOwnerAndCurrencyAndType(sourceOwner, senderWalletType, currency) - ?: throw OpexException(OpexError.WalletNotFound) - - val receiverOwner = walletOwnerManager.findWalletOwner(receiverUuid) ?: walletOwnerManager.createWalletOwner( + return transferService.transfer( + symbol, + senderWalletType, senderUuid, - "not set", - "" - ) - val receiverWallet = walletManager.findWalletByOwnerAndCurrencyAndType( - receiverOwner, receiverWalletType, currency - ) ?: walletManager.createWallet( - receiverOwner, - Amount(currency, BigDecimal.ZERO), - currency, - receiverWalletType + receiverWalletType, + receiverUuid, + amount, + description, + transferRef ) - return transferService.transfer( - TransferCommand( - sourceWallet, - receiverWallet, - Amount(sourceWallet.currency, amount), - description, transferRef, emptyMap() - ) - ).transferResult + } + + @PostMapping("/transfer/batch") + suspend fun batchTransfer(@RequestBody request: List) { + transferService.batchTransfer(request) } @PostMapping("/deposit/{amount}_{symbol}/{receiverUuid}_{receiverWalletType}") @@ -96,34 +69,6 @@ class TransferController( @RequestParam("description") description: String?, @RequestParam("transferRef") transferRef: String? ): TransferResult { - if (receiverWalletType == "cashout") throw OpexException(OpexError.InvalidCashOutUsage) - val systemUuid = "1" - val currency = currencyService.getCurrency(symbol) ?: throw OpexException(OpexError.CurrencyNotFound) - val sourceOwner = walletOwnerManager.findWalletOwner(systemUuid) - ?: throw OpexException(OpexError.WalletOwnerNotFound) - val sourceWallet = walletManager.findWalletByOwnerAndCurrencyAndType(sourceOwner, "main", currency) - ?: throw OpexException(OpexError.WalletNotFound) - - val receiverOwner = walletOwnerManager.findWalletOwner(receiverUuid) ?: walletOwnerManager.createWalletOwner( - systemUuid, - "not set", - "" - ) - val receiverWallet = walletManager.findWalletByOwnerAndCurrencyAndType( - receiverOwner, receiverWalletType, currency - ) ?: walletManager.createWallet( - receiverOwner, - Amount(currency, BigDecimal.ZERO), - currency, - receiverWalletType - ) - return transferService.transfer( - TransferCommand( - sourceWallet, - receiverWallet, - Amount(sourceWallet.currency, amount), - description, transferRef, emptyMap() - ) - ).transferResult + return transferService.deposit(symbol, receiverUuid, receiverWalletType, amount, description, transferRef) } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/TransferRequest.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/TransferRequest.kt new file mode 100644 index 000000000..6b449707c --- /dev/null +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/TransferRequest.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.wallet.app.dto + +import java.math.BigDecimal + +data class TransferRequest( + val amount: BigDecimal, + val symbol: String, + val senderUuid: String, + val senderWalletType: String, + val receiverUuid: String, + val receiverWalletType: String, + val transferRef: String?, + val description: String? +) \ No newline at end of file diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt new file mode 100644 index 000000000..4f3ca3ed8 --- /dev/null +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt @@ -0,0 +1,142 @@ +package co.nilin.opex.wallet.app.service + +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import co.nilin.opex.wallet.app.dto.TransferRequest +import co.nilin.opex.wallet.core.inout.TransferCommand +import co.nilin.opex.wallet.core.inout.TransferResult +import co.nilin.opex.wallet.core.model.Amount +import co.nilin.opex.wallet.core.spi.CurrencyService +import co.nilin.opex.wallet.core.spi.TransferManager +import co.nilin.opex.wallet.core.spi.WalletManager +import co.nilin.opex.wallet.core.spi.WalletOwnerManager +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal + +@Service +class TransferService( + private val transferManager: TransferManager, + private val currencyService: CurrencyService, + private val walletManager: WalletManager, + private val walletOwnerManager: WalletOwnerManager +) { + + @Transactional + suspend fun transfer( + symbol: String, + senderWalletType: String, + senderUuid: String, + receiverWalletType: String, + receiverUuid: String, + amount: BigDecimal, + description: String?, + transferRef: String? + ): TransferResult { + if (senderWalletType == "cashout" || receiverWalletType == "cashout") + throw OpexException(OpexError.InvalidCashOutUsage) + val currency = currencyService.getCurrency(symbol) ?: throw OpexException(OpexError.CurrencyNotFound) + val sourceOwner = walletOwnerManager.findWalletOwner(senderUuid) + ?: throw OpexException(OpexError.WalletOwnerNotFound) + val sourceWallet = walletManager.findWalletByOwnerAndCurrencyAndType(sourceOwner, senderWalletType, currency) + ?: throw OpexException(OpexError.WalletNotFound) + + val receiverOwner = walletOwnerManager.findWalletOwner(receiverUuid) ?: walletOwnerManager.createWalletOwner( + senderUuid, + "not set", + "" + ) + val receiverWallet = walletManager.findWalletByOwnerAndCurrencyAndType( + receiverOwner, receiverWalletType, currency + ) ?: walletManager.createWallet( + receiverOwner, + Amount(currency, BigDecimal.ZERO), + currency, + receiverWalletType + ) + return transferManager.transfer( + TransferCommand( + sourceWallet, + receiverWallet, + Amount(sourceWallet.currency, amount), + description, transferRef, emptyMap() + ) + ).transferResult + } + + @Transactional + suspend fun batchTransfer(request: List) { + request.filter { it.receiverWalletType != "cashout" && it.senderWalletType != "cashout" } + .forEach { + val currency = currencyService.getCurrency(it.symbol) + ?: throw OpexException(OpexError.CurrencyNotFound) + val sourceOwner = walletOwnerManager.findWalletOwner(it.senderUuid) + ?: throw OpexException(OpexError.WalletOwnerNotFound) + val sourceWallet = + walletManager.findWalletByOwnerAndCurrencyAndType(sourceOwner, it.senderWalletType, currency) + ?: throw OpexException(OpexError.WalletNotFound) + + val receiverOwner = walletOwnerManager.findWalletOwner(it.receiverUuid) + ?: walletOwnerManager.createWalletOwner(it.senderUuid, "not set", "") + + val receiverWallet = + walletManager.findWalletByOwnerAndCurrencyAndType(receiverOwner, it.receiverWalletType, currency) + ?: walletManager.createWallet( + receiverOwner, + Amount(currency, BigDecimal.ZERO), + currency, + it.receiverWalletType + ) + transferManager.transfer( + TransferCommand( + sourceWallet, + receiverWallet, + Amount(sourceWallet.currency, it.amount), + it.description, + it.transferRef, + emptyMap() + ) + ) + } + } + + suspend fun deposit( + symbol: String, + receiverUuid: String, + receiverWalletType: String, + amount: BigDecimal, + description: String?, + transferRef: String? + ): TransferResult { + if (receiverWalletType == "cashout") throw OpexException(OpexError.InvalidCashOutUsage) + val systemUuid = "1" + val currency = currencyService.getCurrency(symbol) ?: throw OpexException(OpexError.CurrencyNotFound) + val sourceOwner = walletOwnerManager.findWalletOwner(systemUuid) + ?: throw OpexException(OpexError.WalletOwnerNotFound) + val sourceWallet = walletManager.findWalletByOwnerAndCurrencyAndType(sourceOwner, "main", currency) + ?: throw OpexException(OpexError.WalletNotFound) + + val receiverOwner = walletOwnerManager.findWalletOwner(receiverUuid) ?: walletOwnerManager.createWalletOwner( + systemUuid, + "not set", + "" + ) + val receiverWallet = walletManager.findWalletByOwnerAndCurrencyAndType( + receiverOwner, receiverWalletType, currency + ) ?: walletManager.createWallet( + receiverOwner, + Amount(currency, BigDecimal.ZERO), + currency, + receiverWalletType + ) + return transferManager.transfer( + TransferCommand( + sourceWallet, + receiverWallet, + Amount(sourceWallet.currency, amount), + description, transferRef, emptyMap() + ) + ).transferResult + } + +} \ No newline at end of file diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WalletType.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WalletType.kt new file mode 100644 index 000000000..263083892 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/WalletType.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.wallet.core.inout + +enum class WalletType { + MAIN, EXCHANGE, CASHOUT +} \ No newline at end of file diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt similarity index 89% rename from wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt rename to wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt index 7ef4072c2..e400eaab4 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImpl.kt @@ -10,20 +10,22 @@ import co.nilin.opex.wallet.core.inout.TransferResultDetailed import co.nilin.opex.wallet.core.model.Amount import co.nilin.opex.wallet.core.model.Transaction import co.nilin.opex.wallet.core.spi.* +import org.springframework.stereotype.Component import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime import java.util.* -@Service -class TransferService( - val walletManager: WalletManager, - val walletListener: WalletListener, - val walletOwnerManager: WalletOwnerManager, - val transactionManager: TransactionManager -) { +@Component +class TransferManagerImpl( + private val walletManager: WalletManager, + private val walletListener: WalletListener, + private val walletOwnerManager: WalletOwnerManager, + private val transactionManager: TransactionManager +) : TransferManager { + @Transactional - suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed { + override suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed { //pre transfer hook (dispatch pre transfer event) val srcWallet = transferCommand.sourceWallet val srcWalletOwner = srcWallet.owner diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt index 789de6001..18c4c0e5d 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt @@ -3,10 +3,7 @@ package co.nilin.opex.wallet.core.service import co.nilin.opex.wallet.core.inout.* import co.nilin.opex.wallet.core.model.Amount import co.nilin.opex.wallet.core.model.Withdraw -import co.nilin.opex.wallet.core.spi.CurrencyService -import co.nilin.opex.wallet.core.spi.WalletManager -import co.nilin.opex.wallet.core.spi.WalletOwnerManager -import co.nilin.opex.wallet.core.spi.WithdrawPersister +import co.nilin.opex.wallet.core.spi.* import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,12 +12,12 @@ import java.time.LocalDateTime @Service class WithdrawService( - val withdrawPersister: WithdrawPersister, - val walletManager: WalletManager, - val walletOwnerManager: WalletOwnerManager, - val currencyService: CurrencyService, - val transferService: TransferService, - @Value("\${app.system.uuid}") val systemUuid: String + private val withdrawPersister: WithdrawPersister, + private val walletManager: WalletManager, + private val walletOwnerManager: WalletOwnerManager, + private val currencyService: CurrencyService, + private val transferManager: TransferManager, + @Value("\${app.system.uuid}") private val systemUuid: String ) { @Transactional @@ -38,7 +35,7 @@ class WithdrawService( currency, "cashout" ) - val transferResultDetailed = transferService.transfer( + val transferResultDetailed = transferManager.transfer( TransferCommand( sourceWallet, receiverWallet, @@ -92,7 +89,7 @@ class WithdrawService( sourceWallet.currency, "main" ) - val transferResultDetailed = transferService.transfer( + val transferResultDetailed = transferManager.transfer( TransferCommand( sourceWallet, receiverWallet, @@ -145,7 +142,7 @@ class WithdrawService( sourceWallet.currency, "main" ) - val transferResultDetailed = transferService.transfer( + val transferResultDetailed = transferManager.transfer( TransferCommand( sourceWallet, receiverWallet, diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TransferManager.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TransferManager.kt new file mode 100644 index 000000000..cfd61b9b1 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TransferManager.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.wallet.core.spi + +import co.nilin.opex.wallet.core.inout.TransferCommand +import co.nilin.opex.wallet.core.inout.TransferResultDetailed + +interface TransferManager { + + suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed + +} \ No newline at end of file diff --git a/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferServiceTest.kt b/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImplTest.kt similarity index 93% rename from wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferServiceTest.kt rename to wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImplTest.kt index a66d4faa6..d52adca35 100644 --- a/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferServiceTest.kt +++ b/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferManagerImplTest.kt @@ -14,13 +14,13 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test -private class TransferServiceTest { +private class TransferManagerImplTest { private val walletOwnerManager: WalletOwnerManager = mockk() private val walletManager: WalletManager = mockk() private val walletListener: WalletListener = mockk() private val transactionManager: TransactionManager = mockk() - private val transferService: TransferService = - TransferService(walletManager, walletListener, walletOwnerManager, transactionManager) + private val transferManager: TransferManagerImpl = + TransferManagerImpl(walletManager, walletListener, walletOwnerManager, transactionManager) private fun stubWalletListener() { coEvery { @@ -48,7 +48,7 @@ private class TransferServiceTest { coEvery { walletListener.onDeposit(any(), any(), any(), any(), any(), any()) } returns Unit coEvery { transactionManager.save(any()) } returns "1" - val result = transferService.transfer(VALID.TRANSFER_COMMAND).transferResult + val result = transferManager.transfer(VALID.TRANSFER_COMMAND).transferResult assertThat(result).isNotNull assertThat(result.sourceUuid).isEqualTo(VALID.SOURCE_WALLET_OWNER.uuid) @@ -81,7 +81,7 @@ private class TransferServiceTest { assertThatThrownBy { runBlocking { - transferService.transfer(VALID.TRANSFER_COMMAND) + transferManager.transfer(VALID.TRANSFER_COMMAND) } }.isNotInstanceOf(MockKException::class.java) } @@ -100,7 +100,7 @@ private class TransferServiceTest { assertThatThrownBy { runBlocking { - transferService.transfer(VALID.TRANSFER_COMMAND) + transferManager.transfer(VALID.TRANSFER_COMMAND) } }.isNotInstanceOf(MockKException::class.java) } @@ -119,7 +119,7 @@ private class TransferServiceTest { assertThatThrownBy { runBlocking { - transferService.transfer(VALID.TRANSFER_COMMAND) + transferManager.transfer(VALID.TRANSFER_COMMAND) } }.isNotInstanceOf(MockKException::class.java) } @@ -138,7 +138,7 @@ private class TransferServiceTest { assertThatThrownBy { runBlocking { - transferService.transfer(VALID.TRANSFER_COMMAND) + transferManager.transfer(VALID.TRANSFER_COMMAND) } }.isNotInstanceOf(MockKException::class.java) } @@ -167,7 +167,7 @@ private class TransferServiceTest { assertThatThrownBy { runBlocking { - transferService.transfer(VALID.TRANSFER_COMMAND) + transferManager.transfer(VALID.TRANSFER_COMMAND) } }.isNotInstanceOf(MockKException::class.java) } diff --git a/wallet/wallet-ports/wallet-persister-postgres/pom.xml b/wallet/wallet-ports/wallet-persister-postgres/pom.xml index bdbf040fe..aec02a1aa 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/pom.xml +++ b/wallet/wallet-ports/wallet-persister-postgres/pom.xml @@ -25,7 +25,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt index 904b16593..bdffbf1c3 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt @@ -27,120 +27,99 @@ class WalletOwnerManagerImpl( val walletOwnerRepository: WalletOwnerRepository ) : WalletOwnerManager { - val logger = LoggerFactory.getLogger(WalletOwnerManager::class.java) + private val logger = LoggerFactory.getLogger(WalletOwnerManager::class.java) override suspend fun isDepositAllowed(owner: WalletOwner, amount: Amount): Boolean { require(amount.amount >= BigDecimal.ZERO) - var evaluate: Boolean = limitsRepository.findByOwnerAndAction( - owner.id!!, - "deposit" - ).map { limit -> - evaluateLimit(amount.amount, limit, owner, true) - }.onEmpty { - emit(true) - } - .reduce { a, b -> - a && b - } + var evaluate = limitsRepository.findByOwnerAndAction(owner.id!!, "deposit") + .map { limit -> evaluateLimit(amount.amount, limit, owner, true) } + .onEmpty { emit(true) } + .reduce { a, b -> a && b } + if (evaluate) { - evaluate = limitsRepository.findByLevelAndAction( - owner.level, - "deposit" - ) - .map { limit -> - evaluateLimit(amount.amount, limit, owner, true) - }.onEmpty { - emit(true) - }.reduce { a, b -> - a && b - } + evaluate = limitsRepository.findByLevelAndAction(owner.level, "deposit") + .map { limit -> evaluateLimit(amount.amount, limit, owner, true) } + .onEmpty { emit(true) } + .reduce { a, b -> a && b } } + logger.info("isDepositAllowed: {} {}{} {}", owner.uuid, amount.amount, amount.currency.name, evaluate) return evaluate } + override suspend fun isWithdrawAllowed(owner: WalletOwner, amount: Amount): Boolean { + require(amount.amount >= BigDecimal.ZERO) + var evaluate = limitsRepository.findByOwnerAndAction(owner.id!!, "withdraw") + .map { limit -> evaluateLimit(amount.amount, limit, owner, false) } + .onEmpty { emit(true) } + .reduce { a, b -> a && b } + + if (evaluate) { + evaluate = limitsRepository.findByLevelAndAction(owner.level, "withdraw") + .map { limit -> evaluateLimit(amount.amount, limit, owner, false) } + .onEmpty { emit(true) } + .reduce { a, b -> a && b } + } + + logger.info("isWithdrawAllowed: {} {}{} {}", owner.uuid, amount.amount, amount.currency.name, evaluate) + return evaluate + } + private suspend fun evaluateLimit( amount: BigDecimal, limit: WalletLimitsModel?, owner: WalletOwner, deposit: Boolean ): Boolean { + if (limit == null) + return true + var evaluate = true - if (limit != null) { - val mainCurrency = walletConfigRepository.findAll() - .map { t: WalletConfigModel -> t.mainCurrency } - .awaitFirstOrDefault("BTC") - if (limit.dailyCount != null || limit.dailyTotal != null) { + val mainCurrency = walletConfigRepository.findAll() + .map { it.mainCurrency } + .awaitFirstOrDefault("BTC") + + if (limit.dailyCount != null || limit.dailyTotal != null) { + val ts = if (deposit) { + transactionRepository.calculateDepositStatisticsBasedOnCurrency( + owner.id!!, limit.walletType, LocalDateTime.now().minusDays(1) + .withHour(0).withMinute(0).withSecond(0), LocalDateTime.now(), mainCurrency + ) + } else { + transactionRepository.calculateWithdrawStatisticsBasedOnCurrency( + owner.id!!, limit.walletType, LocalDateTime.now().minusDays(1) + .withHour(0).withMinute(0).withSecond(0), LocalDateTime.now(), mainCurrency + ) + }.awaitFirstOrNull() + evaluate = if (ts != null) { + !((limit.dailyCount != null && ts.cnt!! >= limit.dailyCount) + || (limit.dailyTotal != null && ts.total!! >= limit.dailyTotal)) + } else { + limit.dailyTotal?.let { it >= amount } ?: true + } + } + + if (evaluate) { + if (limit.monthlyCount != null || limit.monthlyTotal != null) { val ts = if (deposit) { transactionRepository.calculateDepositStatisticsBasedOnCurrency( - owner.id!!, limit.walletType, LocalDateTime.now().minusDays(1) + owner.id!!, limit.walletType, LocalDateTime.now().minusMonths(1).withDayOfMonth(1) .withHour(0).withMinute(0).withSecond(0), LocalDateTime.now(), mainCurrency ) } else { transactionRepository.calculateWithdrawStatisticsBasedOnCurrency( - owner.id!!, limit.walletType, LocalDateTime.now().minusDays(1) + owner.id!!, limit.walletType, LocalDateTime.now().minusMonths(1).withDayOfMonth(1) .withHour(0).withMinute(0).withSecond(0), LocalDateTime.now(), mainCurrency ) }.awaitFirstOrNull() evaluate = if (ts != null) { - !((limit.dailyCount != null && ts.cnt!! >= limit.dailyCount) - || (limit.dailyTotal != null && ts.total!! >= limit.dailyTotal)) + !((limit.monthlyCount != null && ts.cnt!! >= limit.monthlyCount) + || (limit.monthlyTotal != null && ts.total!! >= limit.monthlyTotal)) } else { - limit.dailyTotal?.let { it >= amount } ?: true + limit.monthlyTotal?.let { it >= amount } ?: true } } - if (evaluate) { - if (limit.monthlyCount != null || limit.monthlyTotal != null) { - val ts = if (deposit) { - transactionRepository.calculateDepositStatisticsBasedOnCurrency( - owner.id!!, limit.walletType, LocalDateTime.now().minusMonths(1).withDayOfMonth(1) - .withHour(0).withMinute(0).withSecond(0), LocalDateTime.now(), mainCurrency - ) - } else { - transactionRepository.calculateWithdrawStatisticsBasedOnCurrency( - owner.id!!, limit.walletType, LocalDateTime.now().minusMonths(1).withDayOfMonth(1) - .withHour(0).withMinute(0).withSecond(0), LocalDateTime.now(), mainCurrency - ) - }.awaitFirstOrNull() - evaluate = if (ts != null) { - !((limit.monthlyCount != null && ts.cnt!! >= limit.monthlyCount) - || (limit.monthlyTotal != null && ts.total!! >= limit.monthlyTotal)) - } else { - limit.monthlyTotal?.let { it >= amount } ?: true - } - } - } - } - return evaluate - } - - override suspend fun isWithdrawAllowed(owner: WalletOwner, amount: Amount): Boolean { - require(amount.amount >= BigDecimal.ZERO) - var evaluate: Boolean = limitsRepository.findByOwnerAndAction( - owner.id!!, - "withdraw" - ) - .map { limit -> - evaluateLimit(amount.amount, limit, owner, false) - }.onEmpty { - emit(true) - }.reduce { a, b -> - a && b - } - if (evaluate) { - evaluate = limitsRepository.findByLevelAndAction( - owner.level, - "withdraw" - ) - .map { limit -> - evaluateLimit(amount.amount, limit, owner, false) - }.onEmpty { - emit(true) - }.reduce { a, b -> - a && b - } } - logger.info("isWithdrawAllowed: {} {}{} {}", owner.uuid, amount.amount, amount.currency.name, evaluate) return evaluate } diff --git a/websocket/websocket-ports/websocket-persister-postgres/pom.xml b/websocket/websocket-ports/websocket-persister-postgres/pom.xml index 7bb7f7a4f..5dd103d7b 100644 --- a/websocket/websocket-ports/websocket-persister-postgres/pom.xml +++ b/websocket/websocket-ports/websocket-persister-postgres/pom.xml @@ -36,7 +36,7 @@ spring-boot-starter-data-r2dbc - io.r2dbc + org.postgresql r2dbc-postgresql runtime From ca1bbbb22dea834759896b4a99cca53fe69a1e48 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 28 Feb 2023 15:49:02 +0330 Subject: [PATCH 11/11] Add new order param validation --- .../binance/controller/AccountController.kt | 64 +++++++++++++++++++ .../ports/postgres/dao/OrderRepository.kt | 1 + 2 files changed, 65 insertions(+) diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt index 4edfd83d2..4bb21427a 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt @@ -90,6 +90,7 @@ class AccountController( @CurrentSecurityContext securityContext: SecurityContext ): NewOrderResponse { val internalSymbol = symbolMapper.toInternalSymbol(symbol) ?: throw OpexException(OpexError.SymbolNotFound) + validateNewOrderParams(type, price, quantity, timeInForce, stopPrice, quoteOrderQty) matchingGatewayProxy.createNewOrder( securityContext.jwtAuthentication().name, @@ -414,6 +415,69 @@ class AccountController( ) } + private fun validateNewOrderParams( + type: OrderType, + price: BigDecimal?, + quantity: BigDecimal?, + timeInForce: TimeInForce?, + stopPrice: BigDecimal?, + quoteOrderQty: BigDecimal?, + ) { + when (type) { + OrderType.LIMIT -> { + checkDecimal(price, "price") + checkDecimal(quantity, "quantity") + checkNull(timeInForce, "timeInForce") + } + + OrderType.MARKET -> { + if (quantity == null) + checkDecimal(quoteOrderQty, "quoteOrderQty") + else + checkDecimal(quantity, "quantity") + } + + OrderType.STOP_LOSS -> { + checkDecimal(quantity, "quantity") + checkDecimal(stopPrice, "stopPrice") + } + + OrderType.STOP_LOSS_LIMIT -> { + checkDecimal(price, "price") + checkDecimal(quantity, "quantity") + checkDecimal(stopPrice, "stopPrice") + checkNull(timeInForce, "timeInForce") + } + + OrderType.TAKE_PROFIT -> { + checkDecimal(quantity, "quantity") + checkDecimal(stopPrice, "stopPrice") + } + + OrderType.TAKE_PROFIT_LIMIT -> { + checkDecimal(price, "price") + checkDecimal(quantity, "quantity") + checkDecimal(stopPrice, "stopPrice") + checkNull(timeInForce, "timeInForce") + } + + OrderType.LIMIT_MAKER -> { + checkDecimal(price, "price") + checkDecimal(quantity, "quantity") + } + } + } + + private fun checkDecimal(decimal: BigDecimal?, paramName: String) { + if (decimal == null || decimal <= BigDecimal.ZERO) + throw OpexException(OpexError.InvalidRequestParam, "Parameter '$paramName' is either missing or invalid") + } + + private fun checkNull(obj: Any?, paramName: String) { + if (obj == null) + throw OpexException(OpexError.InvalidRequestParam, "Parameter '$paramName' is either missing or invalid") + } + private fun Order.asQueryOrderResponse() = QueryOrderResponse( symbol, ouid, diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt index 2a5e91d4b..810cc540a 100644 --- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt +++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/dao/OrderRepository.kt @@ -44,6 +44,7 @@ interface OrderRepository : ReactiveCrudRepository { where uuid = :uuid and (:symbol is null or symbol = :symbol) and status in (:statuses) and appearance = (select max(appearance) from order_status where ouid = orders.ouid) and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + order by create_date desc limit :limit """ )