From 186389c544344e43e470186adfa49134be993239 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 6 Aug 2025 16:33:30 +0330 Subject: [PATCH 01/12] Add Redis caching support and fee config initialization --- .../opex/accountant/app/config/CacheConfig.kt | 34 ++++++ .../src/main/resources/application.yml | 5 + .../accountant-persister-postgres/pom.xml | 4 + .../postgres/service/FeeConfigInitializer.kt | 53 +++++++++ .../ports/postgres/util/RedisCacheHelper.kt | 105 ++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt new file mode 100644 index 000000000..974a060df --- /dev/null +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt @@ -0,0 +1,34 @@ +package co.nilin.opex.accountant.app.config + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.StringRedisSerializer + +@Configuration +@EnableCaching +class CacheConfig { + + @Bean + fun redisTemplate(connectionFactory: RedisConnectionFactory, mapper: ObjectMapper): RedisTemplate { + val newMapper = mapper.copy().apply { + activateDefaultTyping(mapper.polymorphicTypeValidator, ObjectMapper.DefaultTyping.EVERYTHING) + findAndRegisterModules() + registerKotlinModule() + } + return RedisTemplate().apply { + setConnectionFactory(connectionFactory) + val ser = GenericJackson2JsonRedisSerializer(newMapper) + valueSerializer = ser + hashValueSerializer = ser + keySerializer = StringRedisSerializer() + hashKeySerializer = StringRedisSerializer() + afterPropertiesSet() + } + } +} \ No newline at end of file diff --git a/accountant/accountant-app/src/main/resources/application.yml b/accountant/accountant-app/src/main/resources/application.yml index 25ba3928d..b03020db7 100644 --- a/accountant/accountant-app/src/main/resources/application.yml +++ b/accountant/accountant-app/src/main/resources/application.yml @@ -22,6 +22,11 @@ spring: initial-size: 10 max-size: 32 validation-query: select 1 + cache: + type: redis + redis: + host: redis-cache + port: 6379 cloud: bootstrap: enabled: true diff --git a/accountant/accountant-ports/accountant-persister-postgres/pom.xml b/accountant/accountant-ports/accountant-persister-postgres/pom.xml index b90ec1cf5..d7f2d7d66 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/pom.xml +++ b/accountant/accountant-ports/accountant-persister-postgres/pom.xml @@ -20,6 +20,10 @@ org.jetbrains.kotlin kotlin-reflect + + org.springframework.boot + spring-boot-starter-data-redis + co.nilin.opex.matching.engine.core matching-engine-core diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt new file mode 100644 index 000000000..c0c9fe0f7 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt @@ -0,0 +1,53 @@ +package co.nilin.opex.accountant.ports.postgres.service + +import co.nilin.opex.accountant.ports.postgres.util.RedisCacheHelper +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.math.BigDecimal +import javax.annotation.PostConstruct + +@Service +class FeeConfigInitializer(private val redisCacheHelper: RedisCacheHelper) { + private val logger = LoggerFactory.getLogger(FeeConfigInitializer::class.java) + + @PostConstruct + fun initialize() { + logger.info( + """ +================================================================================ + Starting Fee Configuration Initialization +================================================================================ + """.trimIndent() + ) + try { + putAndLog("fee:maker:user-1", BigDecimal.valueOf(0.5)) + putAndLog("fee:taker:user-1", BigDecimal.valueOf(0.5)) + putAndLog("fee:maker:user-2", BigDecimal.valueOf(0.3)) + putAndLog("fee:taker:user-2", BigDecimal.valueOf(0.3)) + putAndLog("fee:maker:user-3", BigDecimal.valueOf(0.2)) + putAndLog("fee:taker:user-3", BigDecimal.valueOf(0.2)) + + logger.info( + """ +================================================================================ + Fee Configuration Initialization Completed Successfully +================================================================================ + """.trimIndent() + ) + } catch (e: Exception) { + logger.error( + """ +================================================================================ + Error During Fee Configuration Initialization: ${e.message} +================================================================================ + """.trimIndent(), e + ) + throw e + } + } + + private fun putAndLog(key: String, value: BigDecimal) { + redisCacheHelper.put(key, value) + logger.info("Set fee config -> key: '{}', value: '{}'", key, value) + } +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt new file mode 100644 index 000000000..5e3995e8a --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt @@ -0,0 +1,105 @@ +package co.nilin.opex.accountant.ports.postgres.util + +import co.nilin.opex.common.utils.DynamicInterval +import co.nilin.opex.common.utils.LoggerDelegate +import org.springframework.stereotype.Component +import org.springframework.data.redis.core.RedisTemplate + + +@Component +class RedisCacheHelper(private val redisTemplate: RedisTemplate) { + + private val logger by LoggerDelegate() + + private val valueOps = redisTemplate.opsForValue() + private val listOps = redisTemplate.opsForList() + + fun put(key: String, value: Any?, expireAt: DynamicInterval? = null) { + value ?: return + try { + valueOps.set(key, value) + expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) } + } catch (e: Exception) { + logger.warn("Unable to put cache with key '$key'") + } + } + + fun putList(key: String, values: List, expireAt: DynamicInterval? = null) { + try { + values.forEach { listOps.rightPush(key, it) } + expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) } + } catch (e: Exception) { + logger.warn("Unable to put list cache with key '$key'") + } + } + + fun putListItem(key: String, value: Any, rightPush: Boolean = true) { + try { + if (rightPush) + listOps.rightPush(key, value) + else + listOps.leftPush(key, value) + } catch (e: Exception) { + logger.warn("Unable to put list item cache with key '$key'") + } + } + + @Suppress("UNCHECKED_CAST") + fun get(key: String): T? { + return try { + valueOps.get(key) as T + } catch (e: Exception) { + logger.warn("Unable to get cache value with key '$key'") + null + } + } + + @Suppress("UNCHECKED_CAST") + fun getList(key: String): Collection? { + return try { + listOps.range(key, 0, -1) as Collection? + } catch (e: Exception) { + logger.warn("Unable to get cache list with key '$key'") + null + } + } + + suspend fun getOrElse(key: String, expireAt: DynamicInterval? = null, action: suspend () -> T): T { + val cacheValue = get(key) + return if (cacheValue != null) + cacheValue + else { + val value = action() + if (value != null) { + put(key, value) + expireAt?.let { setExpiration(key, it) } + } + return value + } + } + + fun evict(key: String) { + try { + redisTemplate.delete(key) + } catch (e: Exception) { + logger.warn("Unable to evict cache with key '$key'") + } + } + + fun setExpiration(key: String, interval: DynamicInterval) { + try { + redisTemplate.expireAt(key, interval.dateInFuture()) + } catch (e: Exception) { + logger.warn("Unable to set expiration date for cache with key '$key'") + } + } + + fun hasKey(key: String): Boolean { + return try { + redisTemplate.hasKey(key) + } catch (e: Exception) { + logger.warn("Unable fetch info of cache with key '$key'") + false + } + } +} \ No newline at end of file From a84f5bfbdc0a84fd77397166cd853419ca0585c2 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 6 Aug 2025 16:44:17 +0330 Subject: [PATCH 02/12] Update FeeConfigInitializer.kt --- .../postgres/service/FeeConfigInitializer.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt index c0c9fe0f7..50ece12ce 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt @@ -6,10 +6,19 @@ import org.springframework.stereotype.Service import java.math.BigDecimal import javax.annotation.PostConstruct + @Service class FeeConfigInitializer(private val redisCacheHelper: RedisCacheHelper) { + private val logger = LoggerFactory.getLogger(FeeConfigInitializer::class.java) + data class Fee(val maker: BigDecimal, val taker: BigDecimal) + private val initialFees = mapOf( + "user-1" to Fee(BigDecimal("0.5"), BigDecimal("0.6")), + "user-2" to Fee(BigDecimal("0.3"), BigDecimal("0.35")), + "user-3" to Fee(BigDecimal("0.2"), BigDecimal("0.25")) + ) + @PostConstruct fun initialize() { logger.info( @@ -20,12 +29,10 @@ class FeeConfigInitializer(private val redisCacheHelper: RedisCacheHelper) { """.trimIndent() ) try { - putAndLog("fee:maker:user-1", BigDecimal.valueOf(0.5)) - putAndLog("fee:taker:user-1", BigDecimal.valueOf(0.5)) - putAndLog("fee:maker:user-2", BigDecimal.valueOf(0.3)) - putAndLog("fee:taker:user-2", BigDecimal.valueOf(0.3)) - putAndLog("fee:maker:user-3", BigDecimal.valueOf(0.2)) - putAndLog("fee:taker:user-3", BigDecimal.valueOf(0.2)) + initialFees.forEach { (user, fee) -> + putAndLog("fee:maker:$user", fee.maker) + putAndLog("fee:taker:$user", fee.taker) + } logger.info( """ From 908905c60c2de94a05b390b31e26b79ef365064e Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 12 Aug 2025 18:06:11 +0330 Subject: [PATCH 03/12] Start variable fee --- .../ports/postgres/model/UserFeeModel.kt | 14 + .../src/main/resources/schema.sql | 348 ++++++------------ 2 files changed, 117 insertions(+), 245 deletions(-) create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt new file mode 100644 index 000000000..abed413cf --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.accountant.ports.postgres.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal + +@Table("user_fee") +data class UserFeeModel( + val uuid: String, + val quoteSymbol: String, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + @Id val id: Long? = null +) \ 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 c541c8e59..48f821b3d 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql @@ -1,276 +1,134 @@ CREATE TABLE IF NOT EXISTS orders ( - id - SERIAL - PRIMARY - KEY, - ouid - VARCHAR -( - 72 -) NOT NULL UNIQUE, - uuid VARCHAR -( - 72 -) NOT NULL, - pair VARCHAR -( - 72 -) NOT NULL, - matching_engine_id INTEGER, - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - left_side_fraction DECIMAL NOT NULL, - right_side_fraction DECIMAL NOT NULL, - user_level VARCHAR -( - 20 -) NOT NULL, - direction VARCHAR -( - 20 -) NOT NULL, - match_constraint VARCHAR -( - 30 -) NOT NULL, - order_type VARCHAR -( - 30 -) NOT NULL, - price DECIMAL NOT NULL, - quantity DECIMAL NOT NULL, - filled_quantity DECIMAL NOT NULL, - orig_price DECIMAL NOT NULL, - orig_quantity DECIMAL NOT NULL, - filled_orig_quantity DECIMAL NOT NULL, - first_transfer_amount DECIMAL NOT NULL, - remained_transfer_amount DECIMAL NOT NULL, - status INTEGER NOT NULL, - agent VARCHAR -( - 20 -), - ip VARCHAR -( - 11 -), - create_date TIMESTAMP NOT NULL - ); + id SERIAL PRIMARY KEY, + ouid VARCHAR(72) NOT NULL UNIQUE, + uuid VARCHAR(72) NOT NULL, + pair VARCHAR(72) NOT NULL, + matching_engine_id INTEGER, + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL, + left_side_fraction DECIMAL NOT NULL, + right_side_fraction DECIMAL NOT NULL, + user_level VARCHAR(20) NOT NULL, + direction VARCHAR(20) NOT NULL, + match_constraint VARCHAR(30) NOT NULL, + order_type VARCHAR(30) NOT NULL, + price DECIMAL NOT NULL, + quantity DECIMAL NOT NULL, + filled_quantity DECIMAL NOT NULL, + orig_price DECIMAL NOT NULL, + orig_quantity DECIMAL NOT NULL, + filled_orig_quantity DECIMAL NOT NULL, + first_transfer_amount DECIMAL NOT NULL, + remained_transfer_amount DECIMAL NOT NULL, + status INTEGER NOT NULL, + agent VARCHAR(20), + ip VARCHAR(11), + create_date TIMESTAMP NOT NULL +); CREATE TABLE IF NOT EXISTS fi_actions ( - id - SERIAL - PRIMARY - KEY, - uuid - VARCHAR -( - 72 -) NOT NULL UNIQUE, - parent_id INTEGER, - event_type VARCHAR -( - 72 -) NOT NULL, - pointer VARCHAR -( - 72 -) NOT NULL, - symbol VARCHAR -( - 36 -) NOT NULL, - amount DECIMAL NOT NULL, - sender VARCHAR -( - 36 -) NOT NULL, - sender_wallet_type VARCHAR -( - 36 -) NOT NULL, - receiver VARCHAR -( - 36 -) NOT NULL, - receiver_wallet_type VARCHAR -( - 36 -) NOT NULL, - agent VARCHAR -( - 20 -), - ip VARCHAR -( - 11 -), - create_date TIMESTAMP NOT NULL, - status VARCHAR -( - 20 -) - ); -CREATE INDEX IF NOT EXISTS idx_fi_actions_symbol ON fi_actions(symbol); -CREATE INDEX IF NOT EXISTS idx_fi_event_type ON fi_actions(event_type); -CREATE INDEX IF NOT EXISTS idx_fi_actions_status ON fi_actions(status); -CREATE INDEX IF NOT EXISTS idx_fi_actions_pointer ON fi_actions(pointer); + id SERIAL PRIMARY KEY, + uuid VARCHAR(72) NOT NULL UNIQUE, + parent_id INTEGER, + event_type VARCHAR(72) NOT NULL, + pointer VARCHAR(72) NOT NULL, + symbol VARCHAR(36) NOT NULL, + amount DECIMAL NOT NULL, + sender VARCHAR(36) NOT NULL, + sender_wallet_type VARCHAR(36) NOT NULL, + receiver VARCHAR(36) NOT NULL, + receiver_wallet_type VARCHAR(36) NOT NULL, + agent VARCHAR(20), + ip VARCHAR(11), + create_date TIMESTAMP NOT NULL, + status VARCHAR(20) +); +CREATE INDEX IF NOT EXISTS idx_fi_actions_symbol ON fi_actions (symbol); +CREATE INDEX IF NOT EXISTS idx_fi_event_type ON fi_actions (event_type); +CREATE INDEX IF NOT EXISTS idx_fi_actions_status ON fi_actions (status); +CREATE INDEX IF NOT EXISTS idx_fi_actions_pointer ON fi_actions (pointer); ALTER TABLE fi_actions - ADD COLUMN IF NOT EXISTS category_name VARCHAR (36); + ADD COLUMN IF NOT EXISTS category_name VARCHAR(36); CREATE TABLE IF NOT EXISTS fi_action_retry ( - id - SERIAL - PRIMARY - KEY, - fa_id - INTEGER - NOT - NULL - UNIQUE - REFERENCES - fi_actions -( - id -), - retries INTEGER NOT NULL DEFAULT 0, + id SERIAL PRIMARY KEY, + fa_id INTEGER NOT NULL UNIQUE REFERENCES fi_actions (id), + retries INTEGER NOT NULL DEFAULT 0, next_run_time TIMESTAMP NOT NULL, - is_resolved BOOLEAN NOT NULL DEFAULT false, - has_given_up BOOLEAN NOT NULL DEFAULT false - ); + is_resolved BOOLEAN NOT NULL DEFAULT false, + has_given_up BOOLEAN NOT NULL DEFAULT false +); CREATE TABLE IF NOT EXISTS fi_action_error ( - id - SERIAL - PRIMARY - KEY, - fa_id - INTEGER - NOT - NULL - REFERENCES - fi_actions -( - id -), - error TEXT NOT NULL, - message TEXT NOT NULL, - body TEXT, - retry_id INTEGER REFERENCES fi_action_retry -( - id -), - date TIMESTAMP NOT NULL DEFAULT CURRENT_DATE - ); + id SERIAL PRIMARY KEY, + fa_id INTEGER NOT NULL REFERENCES fi_actions (id), + error TEXT NOT NULL, + message TEXT NOT NULL, + body TEXT, + retry_id INTEGER REFERENCES fi_action_retry (id), + date TIMESTAMP NOT NULL DEFAULT CURRENT_DATE +); CREATE TABLE IF NOT EXISTS pair_config ( - pair VARCHAR -( - 72 -) PRIMARY KEY, - left_side_wallet_symbol VARCHAR -( - 36 -) NOT NULL, - right_side_wallet_symbol VARCHAR -( - 36 -) NOT NULL, - left_side_fraction DECIMAL NOT NULL, - right_side_fraction DECIMAL NOT NULL, - UNIQUE -( - left_side_wallet_symbol, - right_side_wallet_symbol -) - ); + pair VARCHAR(72) PRIMARY KEY, + left_side_wallet_symbol VARCHAR(36) NOT NULL, + right_side_wallet_symbol VARCHAR(36) NOT NULL, + left_side_fraction DECIMAL NOT NULL, + right_side_fraction DECIMAL NOT NULL, + UNIQUE (left_side_wallet_symbol, right_side_wallet_symbol) +); CREATE TABLE IF NOT EXISTS user_level ( - level VARCHAR -( - 36 -) PRIMARY KEY - ); + level VARCHAR(36) PRIMARY KEY +); 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 REFERENCES user_level -( - level -), - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - UNIQUE -( - direction, - user_level, - pair_config_id -) - ); + id SERIAL PRIMARY KEY, + pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), + direction VARCHAR(36) NOT NULL, + user_level VARCHAR(36) NOT NULL REFERENCES user_level (level), + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL, + UNIQUE (direction, user_level, pair_config_id) +); 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 -) - ); + 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, - ouid - VARCHAR -( - 72 -) NOT NULL, - event_type VARCHAR -( - 72 -) NOT NULL, - event_body TEXT NOT NULL, - event_date TIMESTAMP NOT NULL - ); + id SERIAL PRIMARY KEY, + ouid VARCHAR(72) NOT NULL, + event_type VARCHAR(72) NOT NULL, + event_body TEXT NOT NULL, + event_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_fee +( + id SERIAL PRIMARY KEY, + uuid VARCHAR(32) NOT NULL UNIQUE, + quote_symbol VARCHAR(10) NOT NULL, + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_user_fee_quote_symbol ON user_fee (quote_symbol); + +INSERT INTO user_fee (uuid, quote_symbol, maker_fee, taker_fee) +values ('DEFAULT', 'USDT', 0.0025, 0.005), + ('DEFAULT', 'IRT', 0.0025, 0.005) +ON CONFLICT DO NOTHING; COMMIT; From 5a3e164221f9883d3dda122753ca73e6b5affb62 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 16 Aug 2025 18:03:51 +0330 Subject: [PATCH 04/12] Add fee config services --- .../opex/accountant/app/config/CacheConfig.kt | 34 ------ .../app/controller/PairConfigController.kt | 12 +- .../opex/accountant/core/model/FeeConfig.kt | 16 +++ .../accountant/core/spi/FeeConfigService.kt | 14 +++ .../accountant-persister-postgres/pom.xml | 4 - .../ports/postgres/dao/FeeConfigRepository.kt | 44 ++++++++ .../postgres/impl/FeeConfigServiceImpl.kt | 64 +++++++++++ .../ports/postgres/model/FeeConfigModel.kt | 21 ++++ .../postgres/service/FeeConfigInitializer.kt | 60 ---------- .../ports/postgres/util/RedisCacheHelper.kt | 105 ------------------ .../src/main/resources/schema.sql | 12 ++ .../co/nilin/opex/api/core/inout/FeeConfig.kt | 17 +++ .../opex/api/core/spi/AccountantProxy.kt | 3 +- .../binance/controller/WalletController.kt | 66 +++++------ .../binance/data/ExchangeInfoResponse.kt | 4 +- .../ports/opex/controller/MarketController.kt | 8 +- .../ports/proxy/impl/AccountantProxyImpl.kt | 5 +- .../kotlin/co/nilin/opex/common/OpexError.kt | 2 + 18 files changed, 241 insertions(+), 250 deletions(-) delete mode 100644 accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt deleted file mode 100644 index 974a060df..000000000 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/CacheConfig.kt +++ /dev/null @@ -1,34 +0,0 @@ -package co.nilin.opex.accountant.app.config - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.registerKotlinModule -import org.springframework.cache.annotation.EnableCaching -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.data.redis.connection.RedisConnectionFactory -import org.springframework.data.redis.core.RedisTemplate -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer -import org.springframework.data.redis.serializer.StringRedisSerializer - -@Configuration -@EnableCaching -class CacheConfig { - - @Bean - fun redisTemplate(connectionFactory: RedisConnectionFactory, mapper: ObjectMapper): RedisTemplate { - val newMapper = mapper.copy().apply { - activateDefaultTyping(mapper.polymorphicTypeValidator, ObjectMapper.DefaultTyping.EVERYTHING) - findAndRegisterModules() - registerKotlinModule() - } - return RedisTemplate().apply { - setConnectionFactory(connectionFactory) - val ser = GenericJackson2JsonRedisSerializer(newMapper) - valueSerializer = ser - hashValueSerializer = ser - keySerializer = StringRedisSerializer() - hashKeySerializer = StringRedisSerializer() - afterPropertiesSet() - } - } -} \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt index 962218b17..5049be287 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt @@ -1,8 +1,10 @@ package co.nilin.opex.accountant.app.controller import co.nilin.opex.accountant.app.data.PairFeeResponse +import co.nilin.opex.accountant.core.model.FeeConfig import co.nilin.opex.accountant.core.model.PairConfig import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.FeeConfigService import co.nilin.opex.accountant.core.spi.PairConfigLoader import co.nilin.opex.common.OpexError import co.nilin.opex.matching.engine.core.model.OrderDirection @@ -10,7 +12,10 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/config") -class PairConfigController(private val pairConfigLoader: PairConfigLoader) { +class PairConfigController( + private val pairConfigLoader: PairConfigLoader, + private val feeConfigService: FeeConfigService +) { @GetMapping("/{pair}/fee/{direction}-{userLevel}") suspend fun fetchPairFeeConfig( @@ -35,9 +40,8 @@ class PairConfigController(private val pairConfigLoader: PairConfigLoader) { } @GetMapping("/fee") - suspend fun getFeeConfigs(): List { - return pairConfigLoader.loadPairFeeConfigs() - .map { PairFeeResponse(it.pairConfig.pair, it.direction, it.userLevel, it.makerFee, it.takerFee) } + suspend fun getFeeConfigs(): List { + return feeConfigService.loadFeeConfigs() } @GetMapping("/fee/{pair}") diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt new file mode 100644 index 000000000..00df04871 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeConfig.kt @@ -0,0 +1,16 @@ +package co.nilin.opex.accountant.core.model + +import java.math.BigDecimal + +data class FeeConfig( + val name: String, + val displayOrder: Int, + val minAssetVolume: BigDecimal, + val maxAssetVolume: BigDecimal? = null, + val minTradeVolume: BigDecimal, + val maxTradeVolume: BigDecimal? = null, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + val condition: Condition +) +enum class Condition { AND, OR } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt new file mode 100644 index 000000000..e61f45d05 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.accountant.core.spi + +import co.nilin.opex.accountant.core.model.FeeConfig +import java.math.BigDecimal + +interface FeeConfigService { + + suspend fun loadFeeConfigs(): List + suspend fun saveFee(config: FeeConfig) + suspend fun updateFee(config: FeeConfig) + suspend fun loadFeeConfig(name: String): FeeConfig + suspend fun loadMatchingFeeConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): FeeConfig + +} \ 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 d7f2d7d66..b90ec1cf5 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/pom.xml +++ b/accountant/accountant-ports/accountant-persister-postgres/pom.xml @@ -20,10 +20,6 @@ org.jetbrains.kotlin kotlin-reflect - - org.springframework.boot - spring-boot-starter-data-redis - co.nilin.opex.matching.engine.core matching-engine-core diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt new file mode 100644 index 000000000..7c4d1f8f7 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt @@ -0,0 +1,44 @@ +package co.nilin.opex.accountant.ports.postgres.dao + +import co.nilin.opex.accountant.core.model.FeeConfig +import co.nilin.opex.accountant.ports.postgres.model.FeeConfigModel +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.math.BigDecimal + +@Repository +interface FeeConfigRepository : ReactiveCrudRepository { + + @Query("select * from fee_config order by display_order") + fun findAllByOrder(): Flux + + fun findByName(name: String): Mono + + @Query( + """ + SELECT * + FROM fee_config + WHERE + ( + condition = 'AND' + AND (:assetVolume BETWEEN min_asset_volume AND COALESCE(max_asset_volume, :assetVolume)) + AND (:tradeVolume BETWEEN min_trade_volume AND COALESCE(max_trade_volume, :tradeVolume)) + ) + OR + ( + condition = 'OR' + AND ( + (:assetVolume BETWEEN min_asset_volume AND COALESCE(max_asset_volume, :assetVolume)) + OR (:tradeVolume BETWEEN min_trade_volume AND COALESCE(max_trade_volume, :tradeVolume)) + ) + ) + ORDER BY display_order DESC + LIMIT 1 + """ + ) + fun findMatchingConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): Mono + +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt new file mode 100644 index 000000000..4318627c2 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt @@ -0,0 +1,64 @@ +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.FeeConfig +import co.nilin.opex.accountant.core.spi.FeeConfigService +import co.nilin.opex.accountant.ports.postgres.dao.FeeConfigRepository +import co.nilin.opex.accountant.ports.postgres.model.FeeConfigModel +import co.nilin.opex.common.OpexError +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal + +@Component +class FeeConfigServiceImpl( + private val feeConfigRepository: FeeConfigRepository, +) : FeeConfigService { + + override suspend fun loadFeeConfigs(): List { + return feeConfigRepository.findAllByOrder() + .collectList() + .awaitFirstOrElse { emptyList() } + + } + + override suspend fun saveFee(config: FeeConfig) { + feeConfigRepository.findByName(config.name).awaitFirstOrNull()?.let { + throw OpexError.FeeConfigAlreadyExists.exception("FeeConfig with name '${config.name}' already exists") + } + feeConfigRepository.save(config.asFeeConfigModel()).awaitFirst() + } + + override suspend fun updateFee(config: FeeConfig) { + feeConfigRepository.findByName(config.name).awaitFirstOrNull() + ?: throw OpexError.FeeConfigNotFound.exception("FeeConfig with name '${config.name}' not found") + + feeConfigRepository.save(config.asFeeConfigModel()).awaitFirst() + } + + override suspend fun loadFeeConfig(name: String): FeeConfig { + return feeConfigRepository.findByName(name).awaitFirstOrNull() + ?: throw OpexError.FeeConfigNotFound.exception("FeeConfig with name '${name}' not found") + } + + override suspend fun loadMatchingFeeConfig( + assetVolume: BigDecimal, + tradeVolume: BigDecimal + ): FeeConfig { + return feeConfigRepository.findMatchingConfig(assetVolume, tradeVolume).awaitFirst() + } + + private fun FeeConfig.asFeeConfigModel() = + FeeConfigModel( + name, + displayOrder, + minAssetVolume, + maxAssetVolume, + minTradeVolume, + maxTradeVolume, + makerFee, + takerFee, + condition + ) +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt new file mode 100644 index 000000000..49446a51c --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FeeConfigModel.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.accountant.ports.postgres.model + +import co.nilin.opex.accountant.core.model.Condition +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal + +@Table(name = "fee_config") +data class FeeConfigModel( + @Id + val name: String, + val displayOrder: Int, + val minAssetVolume: BigDecimal, + val maxAssetVolume: BigDecimal? = null, + val minTradeVolume: BigDecimal, + val maxTradeVolume: BigDecimal? = null, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + val condition: Condition, +) + diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt deleted file mode 100644 index 50ece12ce..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/service/FeeConfigInitializer.kt +++ /dev/null @@ -1,60 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.service - -import co.nilin.opex.accountant.ports.postgres.util.RedisCacheHelper -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Service -import java.math.BigDecimal -import javax.annotation.PostConstruct - - -@Service -class FeeConfigInitializer(private val redisCacheHelper: RedisCacheHelper) { - - private val logger = LoggerFactory.getLogger(FeeConfigInitializer::class.java) - - data class Fee(val maker: BigDecimal, val taker: BigDecimal) - private val initialFees = mapOf( - "user-1" to Fee(BigDecimal("0.5"), BigDecimal("0.6")), - "user-2" to Fee(BigDecimal("0.3"), BigDecimal("0.35")), - "user-3" to Fee(BigDecimal("0.2"), BigDecimal("0.25")) - ) - - @PostConstruct - fun initialize() { - logger.info( - """ -================================================================================ - Starting Fee Configuration Initialization -================================================================================ - """.trimIndent() - ) - try { - initialFees.forEach { (user, fee) -> - putAndLog("fee:maker:$user", fee.maker) - putAndLog("fee:taker:$user", fee.taker) - } - - logger.info( - """ -================================================================================ - Fee Configuration Initialization Completed Successfully -================================================================================ - """.trimIndent() - ) - } catch (e: Exception) { - logger.error( - """ -================================================================================ - Error During Fee Configuration Initialization: ${e.message} -================================================================================ - """.trimIndent(), e - ) - throw e - } - } - - private fun putAndLog(key: String, value: BigDecimal) { - redisCacheHelper.put(key, value) - logger.info("Set fee config -> key: '{}', value: '{}'", key, value) - } -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt deleted file mode 100644 index 5e3995e8a..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/util/RedisCacheHelper.kt +++ /dev/null @@ -1,105 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.util - -import co.nilin.opex.common.utils.DynamicInterval -import co.nilin.opex.common.utils.LoggerDelegate -import org.springframework.stereotype.Component -import org.springframework.data.redis.core.RedisTemplate - - -@Component -class RedisCacheHelper(private val redisTemplate: RedisTemplate) { - - private val logger by LoggerDelegate() - - private val valueOps = redisTemplate.opsForValue() - private val listOps = redisTemplate.opsForList() - - fun put(key: String, value: Any?, expireAt: DynamicInterval? = null) { - value ?: return - try { - valueOps.set(key, value) - expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) } - } catch (e: Exception) { - logger.warn("Unable to put cache with key '$key'") - } - } - - fun putList(key: String, values: List, expireAt: DynamicInterval? = null) { - try { - values.forEach { listOps.rightPush(key, it) } - expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) } - } catch (e: Exception) { - logger.warn("Unable to put list cache with key '$key'") - } - } - - fun putListItem(key: String, value: Any, rightPush: Boolean = true) { - try { - if (rightPush) - listOps.rightPush(key, value) - else - listOps.leftPush(key, value) - } catch (e: Exception) { - logger.warn("Unable to put list item cache with key '$key'") - } - } - - @Suppress("UNCHECKED_CAST") - fun get(key: String): T? { - return try { - valueOps.get(key) as T - } catch (e: Exception) { - logger.warn("Unable to get cache value with key '$key'") - null - } - } - - @Suppress("UNCHECKED_CAST") - fun getList(key: String): Collection? { - return try { - listOps.range(key, 0, -1) as Collection? - } catch (e: Exception) { - logger.warn("Unable to get cache list with key '$key'") - null - } - } - - suspend fun getOrElse(key: String, expireAt: DynamicInterval? = null, action: suspend () -> T): T { - val cacheValue = get(key) - return if (cacheValue != null) - cacheValue - else { - val value = action() - if (value != null) { - put(key, value) - expireAt?.let { setExpiration(key, it) } - } - return value - } - } - - fun evict(key: String) { - try { - redisTemplate.delete(key) - } catch (e: Exception) { - logger.warn("Unable to evict cache with key '$key'") - } - } - - fun setExpiration(key: String, interval: DynamicInterval) { - try { - redisTemplate.expireAt(key, interval.dateInFuture()) - } catch (e: Exception) { - logger.warn("Unable to set expiration date for cache with key '$key'") - } - } - - fun hasKey(key: String): Boolean { - return try { - redisTemplate.hasKey(key) - } catch (e: Exception) { - logger.warn("Unable fetch info of cache with key '$key'") - false - } - } -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql index c541c8e59..c9ddef0d2 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 @@ -273,4 +273,16 @@ CREATE TABLE IF NOT EXISTS temp_events event_date TIMESTAMP NOT NULL ); + CREATE TABLE IF NOT EXISTS fee_config ( + name VARCHAR(50) PRIMARY KEY, + display_order INTEGER NOT NULL UNIQUE, + min_asset_volume Decimal NOT NULL, + max_asset_volume Decimal, + min_trade_volume Decimal NOT NULL, + max_trade_volume Decimal, + maker_fee Decimal NOT NULL, + taker_fee Decimal NOT NULL, + condition VARCHAR(10) NOT NULL + ); + COMMIT; diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt new file mode 100644 index 000000000..e7f29715b --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/FeeConfig.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.api.core.inout + +import java.math.BigDecimal + +data class FeeConfig( + val name: String, + val displayOrder: Int, + val minAssetVolume: BigDecimal, + val maxAssetVolume: BigDecimal? = null, + val minTradeVolume: BigDecimal, + val maxTradeVolume: BigDecimal? = null, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + val condition: Condition +) +enum class Condition { AND, OR } + diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt index 14124a738..06cbc0587 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt @@ -1,5 +1,6 @@ package co.nilin.opex.api.core.spi +import co.nilin.opex.api.core.inout.FeeConfig import co.nilin.opex.api.core.inout.PairFeeResponse import co.nilin.opex.api.core.inout.PairConfigResponse @@ -7,7 +8,7 @@ interface AccountantProxy { fun getPairConfigs(): List - fun getFeeConfigs(): List + fun getFeeConfigs(): List fun getFeeConfig(symbol: String): PairFeeResponse diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt index f601a756f..550fed030 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/WalletController.kt @@ -188,39 +188,39 @@ class WalletController( // } // } - @GetMapping("/v1/asset/tradeFee") - fun getPairFees( - @RequestParam(required = false) - symbol: String?, - @RequestParam(required = false) - recvWindow: Long?, //The value cannot be greater than 60000 - @RequestParam - timestamp: Long - ): List { - return if (symbol != null) { - val internalSymbol = symbolMapper.toInternalSymbol(symbol) ?: throw OpexError.SymbolNotFound.exception() - - val fee = accountantProxy.getFeeConfig(internalSymbol) - arrayListOf().apply { - add( - PairFeeResponse( - symbol, - fee.makerFee.toDouble(), - fee.takerFee.toDouble() - ) - ) - } - } else - accountantProxy.getFeeConfigs() - .distinctBy { it.pair } - .map { - PairFeeResponse( - symbolMapper.fromInternalSymbol(it.pair) ?: it.pair, - it.makerFee.toDouble(), - it.takerFee.toDouble() - ) - } - } +// @GetMapping("/v1/asset/tradeFee") +// fun getPairFees( +// @RequestParam(required = false) +// symbol: String?, +// @RequestParam(required = false) +// recvWindow: Long?, //The value cannot be greater than 60000 +// @RequestParam +// timestamp: Long +// ): List { +// return if (symbol != null) { +// val internalSymbol = symbolMapper.toInternalSymbol(symbol) ?: throw OpexError.SymbolNotFound.exception() +// +// val fee = accountantProxy.getFeeConfig(internalSymbol) +// arrayListOf().apply { +// add( +// PairFeeResponse( +// symbol, +// fee.makerFee.toDouble(), +// fee.takerFee.toDouble() +// ) +// ) +// } +// } else +// accountantProxy.getFeeConfigs() +// .distinctBy { it.pair } +// .map { +// PairFeeResponse( +// symbolMapper.fromInternalSymbol(it.pair) ?: it.pair, +// it.makerFee.toDouble(), +// it.takerFee.toDouble() +// ) +// } +// } @GetMapping("/v1/asset/getUserAsset") fun getUserAssets( diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt index d2cbdc718..f27c92a21 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/data/ExchangeInfoResponse.kt @@ -1,6 +1,6 @@ package co.nilin.opex.api.ports.binance.data -import co.nilin.opex.api.core.inout.PairFeeResponse +import co.nilin.opex.api.core.inout.FeeConfig import java.util.* data class ExchangeInfoResponse( @@ -9,6 +9,6 @@ data class ExchangeInfoResponse( val rateLimits: List = RateLimit.values() .map { RateLimitResponse(it.rateLimitType, it.interval, it.intervalNum, it.limit) }, val exchangeFilters: List = emptyList(), - val fees: List = emptyList(), + val fees: List = emptyList(), val symbols: List ) \ No newline at end of file diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt index da9cedfef..4f7793157 100644 --- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt @@ -8,8 +8,6 @@ import co.nilin.opex.api.ports.opex.data.OrderBookResponse import co.nilin.opex.api.ports.opex.data.RecentTradeResponse import co.nilin.opex.common.OpexError import co.nilin.opex.common.utils.Interval -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import org.springframework.web.bind.annotation.* import java.math.BigDecimal import java.time.ZoneId @@ -58,8 +56,8 @@ class MarketController( return walletProxy.getGateWays(includeOffChainGateways, includeOnChainGateways) } - @GetMapping("/pair/fee") - fun getPairFees(): List { + @GetMapping("/fee") + fun getPairFees(): List { return accountantProxy.getFeeConfigs() } @@ -67,7 +65,7 @@ class MarketController( fun getMarketStats( @RequestParam interval: String, @RequestParam(required = false) limit: Int? - ): MarketStatResponse { + ): MarketStatResponse { val intervalEnum = Interval.findByLabel(interval) ?: Interval.Week val validLimit = getValidLimit(limit) diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt index f8c82a8a6..2f58e0771 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt @@ -1,5 +1,6 @@ package co.nilin.opex.api.ports.proxy.impl +import co.nilin.opex.api.core.inout.FeeConfig import co.nilin.opex.api.core.inout.PairConfigResponse import co.nilin.opex.api.core.inout.PairFeeResponse import co.nilin.opex.api.core.spi.AccountantProxy @@ -28,9 +29,9 @@ class AccountantProxyImpl(private val restTemplate: RestTemplate) : AccountantPr ).body?.toList() ?: emptyList() } - override fun getFeeConfigs(): List { + override fun getFeeConfigs(): List { logger.info("fetching fee configs") - return restTemplate.exchange>( + return restTemplate.exchange>( "$baseUrl/config/fee", HttpMethod.GET, noBody() diff --git a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt index 8ed892852..202667dc0 100644 --- a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt +++ b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt @@ -23,6 +23,8 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus InvalidPair(2001, "%s is not available", HttpStatus.BAD_REQUEST), InvalidPairFee(2002, "%s fee is not available", HttpStatus.BAD_REQUEST), PairFeeNotFound(2002, "No fee for requested pair found", HttpStatus.NOT_FOUND), + FeeConfigAlreadyExists(2003, "Fee config already exists", HttpStatus.CONFLICT), + FeeConfigNotFound(2004, "Fee config not found", HttpStatus.NOT_FOUND), // code 3000: matching-engine From 4b1c43c2ca926b2d78ac3ba138d8d1768a968661 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 16 Aug 2025 18:07:00 +0330 Subject: [PATCH 05/12] Update application.yml --- accountant/accountant-app/src/main/resources/application.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/accountant/accountant-app/src/main/resources/application.yml b/accountant/accountant-app/src/main/resources/application.yml index b03020db7..25ba3928d 100644 --- a/accountant/accountant-app/src/main/resources/application.yml +++ b/accountant/accountant-app/src/main/resources/application.yml @@ -22,11 +22,6 @@ spring: initial-size: 10 max-size: 32 validation-query: select 1 - cache: - type: redis - redis: - host: redis-cache - port: 6379 cloud: bootstrap: enabled: true From 3c8d828355c3e9cc97eb0b4b9aba3c2c428c4378 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 17 Aug 2025 12:53:09 +0330 Subject: [PATCH 06/12] Add user trade volume and currency rate in accountant --- .../core/inout/UserCurrencyVolume.kt | 14 +++++ .../core/inout/UserTotalVolumeValue.kt | 12 ++++ .../core/service/TradeManagerImpl.kt | 11 ++-- .../core/spi/CurrencyRatePersister.kt | 8 +++ .../core/service/TradeManagerImplTest.kt | 2 +- .../postgres/dao/CurrencyRateRepository.kt | 30 ++++++++++ .../ports/postgres/dao/UserFeeRepository.kt | 12 ++++ .../postgres/dao/UserTradeVolumeRepository.kt | 59 +++++++++++++++++++ .../impl/CurrencyRatePersisterImpl.kt | 15 +++++ .../ports/postgres/model/CurrencyRateModel.kt | 13 ++++ .../postgres/model/UserTradeVolumeModel.kt | 20 +++++++ .../src/main/resources/schema.sql | 21 +++++++ .../market/app/service/ReportingService.kt | 4 +- .../ports/postgres/impl/TradePersisterImpl.kt | 2 +- 14 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserCurrencyVolume.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserTotalVolumeValue.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/CurrencyRateModel.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserCurrencyVolume.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserCurrencyVolume.kt new file mode 100644 index 000000000..34b3df7a8 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserCurrencyVolume.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.accountant.core.inout + +import java.math.BigDecimal + +data class UserCurrencyVolume( + val currency: String, + val volume: BigDecimal, + val valueUSDT: BigDecimal, + val valueIRT: BigDecimal +) { + companion object { + fun zero(currency: String) = UserCurrencyVolume(currency, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO) + } +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserTotalVolumeValue.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserTotalVolumeValue.kt new file mode 100644 index 000000000..ca9a858d8 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/inout/UserTotalVolumeValue.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.accountant.core.inout + +import java.math.BigDecimal + +data class UserTotalVolumeValue( + val valueUSDT: BigDecimal, + val valueIRT: BigDecimal +) { + companion object { + fun zero() = UserTotalVolumeValue(BigDecimal.ZERO, BigDecimal.ZERO) + } +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt index b6c574ea3..864380b23 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt @@ -22,7 +22,7 @@ open class TradeManagerImpl( private val richOrderPublisher: RichOrderPublisher, private val feeCalculator: FeeCalculator, private val financialActionPublisher: FinancialActionPublisher, - private val jsonMapper: JsonMapper + private val currencyRatePersister: CurrencyRatePersister ) : TradeManager { private val logger = LoggerFactory.getLogger(TradeManagerImpl::class.java) @@ -183,6 +183,9 @@ open class TradeManagerImpl( trade.eventDate ) ) + + currencyRatePersister.updateRate(trade.pair.leftSideName, trade.pair.rightSideName, makerPrice) + return financeActionPersister.persist(financialActions) //return financeActionPersister.persist(financialActions).also { publishFinancialActions(it) } } @@ -250,10 +253,4 @@ open class TradeManagerImpl( if (!list.contains(financialAction)) list.add(financialAction) } - - private fun createMap(tradeEvent: TradeEvent, order: Order): Map { - val orderMap: Map = jsonMapper.toMap(order) - val eventMap: Map = jsonMapper.toMap(tradeEvent) - return orderMap + eventMap - } } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt new file mode 100644 index 000000000..7a028a39d --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.accountant.core.spi + +import java.math.BigDecimal + +interface CurrencyRatePersister { + + suspend fun updateRate(base: String, quote: String, rate: BigDecimal) +} \ No newline at end of file diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt index ac5045208..f7059210f 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,6 +26,7 @@ internal class TradeManagerImplTest { private val richTradePublisher = mockk() private val userLevelLoader = mockk() private val financialActionPublisher = mockk() + private val jsonMapper = JsonMapperTestImpl() private val orderManager = OrderManagerImpl( @@ -49,7 +50,6 @@ internal class TradeManagerImplTest { richOrderPublisher, FeeCalculatorImpl("0x0", jsonMapper), financialActionPublisher, - jsonMapper ) init { diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt new file mode 100644 index 000000000..994477f43 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.accountant.ports.postgres.dao + +import co.nilin.opex.accountant.ports.postgres.model.CurrencyRateModel +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.math.BigDecimal + +@Repository +interface CurrencyRateRepository : ReactiveCrudRepository { + + @Query( + """ + insert into currency_rate (base, quote, rate) + values (:base, :quote, :rate) + on conflict (base, quote) + do update set rate = excluded.rate + """ + ) + fun createOrUpdate(base: String, quote: String, rate: BigDecimal): Mono + + @Query("select * from currency_rate where base = :base and quote = :quote and source = :source") + fun findByBaseAndQuote(base: String, quote: String): Mono + + @Query("select * from currency_rate where quote = :quote and source = :source") + fun findAllByQuote(quote: String): Flux + +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt new file mode 100644 index 000000000..f88aa566a --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.accountant.ports.postgres.dao + +import co.nilin.opex.accountant.ports.postgres.model.UserFeeModel +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Mono + +@Repository +interface UserFeeRepository : ReactiveCrudRepository { + + fun findByUuidAndQuoteSymbol(uuid: String, quoteSymbol: String): Mono +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt new file mode 100644 index 000000000..82e3f4ef7 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserTradeVolumeRepository.kt @@ -0,0 +1,59 @@ +package co.nilin.opex.accountant.ports.postgres.dao + +import co.nilin.opex.accountant.core.inout.UserCurrencyVolume +import co.nilin.opex.accountant.core.inout.UserTotalVolumeValue +import co.nilin.opex.accountant.ports.postgres.model.UserTradeVolumeModel +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Mono +import java.math.BigDecimal +import java.time.LocalDate + +@Repository +interface UserTradeVolumeRepository : ReactiveCrudRepository { + + @Query( + """ + insert into user_trade_volume (user_id, currency, date, volume, value_usdt, value_irt) + values (:userId, :currency, :date, :volume, :valueUSDT, :valueIRT) + on conflict (user_id, currency, date) + do update + set volume = user_trade_volume.volume + EXCLUDED.volume, + value_usdt = user_trade_volume.value_usdt + EXCLUDED.value_usdt, + value_irt = user_trade_volume.value_irt + EXCLUDED.value_irt + """ + ) + fun insertOrUpdate( + userId: String, + currency: String, + date: LocalDate, + volume: BigDecimal, + valueUSDT: BigDecimal, + valueIRT: BigDecimal + ): Mono + + @Query( + """ + select sum(value_usdt) as value_USDT, sum(value_irt) as value_IRT + from user_trade_volume + where user_id = :userId and date >= :startDate + group by user_id + """ + ) + fun findTotalValueByUserAndAndDateAfter(userId: String, startDate: LocalDate): Mono + + @Query( + """ + select currency, sum(volume) as volume, sum(value_usdt) as value_USDT, sum(value_irt) as value_IRT + from user_trade_volume + where user_id = :userId and date >= :startDate and currency = :currency + group by currency + """ + ) + fun findByUserAndCurrencyAndDateAfter( + userId: String, + currency: String, + startDate: LocalDate + ): Mono +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt new file mode 100644 index 000000000..0a5a7fde1 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.spi.CurrencyRatePersister +import co.nilin.opex.accountant.ports.postgres.dao.CurrencyRateRepository +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal + +@Component +class CurrencyRatePersisterImpl(private val repository: CurrencyRateRepository) : CurrencyRatePersister { + + override suspend fun updateRate(base: String, quote: String, rate: BigDecimal) { + repository.createOrUpdate(base, quote, rate).awaitSingleOrNull() + } +} \ 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/CurrencyRateModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/CurrencyRateModel.kt new file mode 100644 index 000000000..e3c929322 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/CurrencyRateModel.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.accountant.ports.postgres.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal + +@Table("currency_rate") +data class CurrencyRateModel( + val base: String, + val quote: String, + val rate: BigDecimal, + @Id val id: Long? = null, +) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt new file mode 100644 index 000000000..01c1b6386 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserTradeVolumeModel.kt @@ -0,0 +1,20 @@ +package co.nilin.opex.accountant.ports.postgres.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal +import java.time.LocalDate + +@Table("user_trade_volume") +class UserTradeVolumeModel( + val userId: String, + val currency: String, + val date: LocalDate, + val volume: BigDecimal, + @Column("value_usdt") + val valueUSDT: BigDecimal, + @Column("value_irt") + val valueIRT: BigDecimal, + @Id val id: Long? = null +) \ 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 48f821b3d..5034bbe2f 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 @@ -126,6 +126,27 @@ CREATE TABLE IF NOT EXISTS user_fee ); CREATE INDEX IF NOT EXISTS idx_user_fee_quote_symbol ON user_fee (quote_symbol); +CREATE TABLE IF NOT EXISTS user_trade_volume +( + id SERIAL PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + currency TEXT NOT NULL, + date DATE not null, + volume decimal not null, + value_usdt decimal not null, + value_irt decimal not null, + unique (user_id, currency, date) +); + +CREATE TABLE IF NOT EXISTS currency_rate +( + id SERIAL PRIMARY KEY, + base VARCHAR(25) NOT NULL, + quote VARCHAR(25) NOT NULL, + rate DECIMAL NOT NULL DEFAULT 0, + UNIQUE (base, quote) +); + INSERT INTO user_fee (uuid, quote_symbol, maker_fee, taker_fee) values ('DEFAULT', 'USDT', 0.0025, 0.005), ('DEFAULT', 'IRT', 0.0025, 0.005) diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt index f4a952100..af95b163f 100644 --- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt +++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/service/ReportingService.kt @@ -16,9 +16,9 @@ class ReportingService(private val marketQueryHandler: MarketQueryHandler) { runBlocking { try { val count = marketQueryHandler.numberOfOrders(Interval.FifteenMinutes, null) - logger.info("in the last 30 minutes, the number of orders : $count") + logger.info("In the last 30 minutes, the number of orders : $count") } catch (e: Exception) { - logger.error("Could not report orders cont", e) + logger.error("Could not report orders count", e) } } } diff --git a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/TradePersisterImpl.kt b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/TradePersisterImpl.kt index 652435bfd..7e44f7147 100644 --- a/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/TradePersisterImpl.kt +++ b/market/market-ports/market-persister-postgres/src/main/kotlin/co/nilin/opex/market/ports/postgres/impl/TradePersisterImpl.kt @@ -68,7 +68,7 @@ class TradePersisterImpl( ).awaitFirstOrNull() logger.info("Rate between ${pair[0]} and ${pair[1]} updated") - calculateTradeVolume(trade, pair[0].uppercase(), pair[1].uppercase()) + //calculateTradeVolume(trade, pair[0].uppercase(), pair[1].uppercase()) // Moved to accountant updateCache(trade, tradeEntity) } From 220e6ddfbf10ed16adad081242130a7de4e59690 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 17 Aug 2025 19:45:42 +0330 Subject: [PATCH 07/12] Calculate trade volume --- .../core/service/OrderManagerImpl.kt | 21 +--- .../core/service/TradeManagerImpl.kt | 97 +++++++++++++------ .../core/spi/CurrencyRatePersister.kt | 2 + .../core/spi/UserVolumePersister.kt | 16 +++ .../postgres/dao/CurrencyRateRepository.kt | 4 +- .../impl/CurrencyRatePersisterImpl.kt | 4 + .../postgres/impl/UserVolumePersisterImpl.kt | 23 +++++ 7 files changed, 116 insertions(+), 51 deletions(-) create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt create mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt index d4bfe0b42..7a0067599 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 @@ -21,8 +21,7 @@ open class OrderManagerImpl( private val orderPersister: OrderPersister, private val tempEventPersister: TempEventPersister, private val richOrderPublisher: RichOrderPublisher, - private val financialActionPublisher: FinancialActionPublisher, - private val jsonMapper: JsonMapper, + private val financialActionPublisher: FinancialActionPublisher ) : OrderManager { @Transactional @@ -269,22 +268,4 @@ open class OrderManagerImpl( } } - private fun createMap(rejectOrderEvent: RejectOrderEvent, order: Order): Map { - val orderMap: Map = jsonMapper.toMap(order) - val eventMap: Map = jsonMapper.toMap(rejectOrderEvent) - return orderMap + eventMap - } - - private fun createMap(cancelOrderEvent: CancelOrderEvent, order: Order): Map { - val orderMap: Map = jsonMapper.toMap(order) - val eventMap: Map = jsonMapper.toMap(cancelOrderEvent) - return orderMap + eventMap - } - - private fun createMap(submitOrderEvent: SubmitOrderEvent, order: Order): Map { - val orderMap: Map = jsonMapper.toMap(order) - val eventMap: Map = jsonMapper.toMap(submitOrderEvent) - return orderMap + eventMap - } - } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt index 864380b23..fb1d5a37e 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt @@ -8,9 +8,12 @@ import co.nilin.opex.accountant.core.inout.RichTrade import co.nilin.opex.accountant.core.model.* import co.nilin.opex.accountant.core.spi.* import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.slf4j.LoggerFactory import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal +import java.time.LocalDate import java.time.LocalDateTime open class TradeManagerImpl( @@ -22,7 +25,8 @@ open class TradeManagerImpl( private val richOrderPublisher: RichOrderPublisher, private val feeCalculator: FeeCalculator, private val financialActionPublisher: FinancialActionPublisher, - private val currencyRatePersister: CurrencyRatePersister + private val currencyRatePersister: CurrencyRatePersister, + private val userVolumePersister: UserVolumePersister ) : TradeManager { private val logger = LoggerFactory.getLogger(TradeManagerImpl::class.java) @@ -154,42 +158,77 @@ open class TradeManagerImpl( val takerPrice = trade.takerPrice.toBigDecimal().multiply(takerOrder.rightSideFraction) val makerPrice = trade.makerPrice.toBigDecimal().multiply(makerOrder.rightSideFraction) - richTradePublisher.publish( - RichTrade( - trade.tradeId, - trade.pair.toString(), - trade.takerOuid, - trade.takerUuid, - trade.takerOrderId, - trade.takerDirection, - takerPrice, - takerOrder.origQuantity, - takerOrder.origPrice.multiply(takerOrder.origQuantity), - trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction), - feeActions.takerFeeAction.amount, - feeActions.takerFeeAction.symbol, - trade.makerOuid, - trade.makerUuid, - trade.makerOrderId, - trade.makerDirection, - makerPrice, - makerOrder.origQuantity, - makerOrder.origPrice.multiply(makerOrder.origQuantity), - trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction), - feeActions.makerFeeAction.amount, - feeActions.makerFeeAction.symbol, - makerPrice, - trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction), - trade.eventDate - ) + val richTrade = RichTrade( + trade.tradeId, + trade.pair.toString(), + trade.takerOuid, + trade.takerUuid, + trade.takerOrderId, + trade.takerDirection, + takerPrice, + takerOrder.origQuantity, + takerOrder.origPrice.multiply(takerOrder.origQuantity), + trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction), + feeActions.takerFeeAction.amount, + feeActions.takerFeeAction.symbol, + trade.makerOuid, + trade.makerUuid, + trade.makerOrderId, + trade.makerDirection, + makerPrice, + makerOrder.origQuantity, + makerOrder.origPrice.multiply(makerOrder.origQuantity), + trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction), + feeActions.makerFeeAction.amount, + feeActions.makerFeeAction.symbol, + makerPrice, + trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction), + trade.eventDate ) + richTradePublisher.publish(richTrade) currencyRatePersister.updateRate(trade.pair.leftSideName, trade.pair.rightSideName, makerPrice) + calculateTradeVolume(richTrade, trade.pair.leftSideName, trade.pair.rightSideName) return financeActionPersister.persist(financialActions) //return financeActionPersister.persist(financialActions).also { publishFinancialActions(it) } } + private suspend fun calculateTradeVolume(trade: RichTrade, base: String, quote: String) { + val today = LocalDate.now() + + var valueUSDT = BigDecimal.ZERO + var valueIRT = BigDecimal.ZERO + + if (quote.equals("IRT", true)) { + val baseUSDTRate = currencyRatePersister.getRate(base, "USDT") + valueUSDT = trade.matchedQuantity * baseUSDTRate + valueIRT = trade.matchedQuantity * trade.matchedPrice + } else if (quote.equals("USDT", true)) { + val baseIRTRate = currencyRatePersister.getRate(base, "IRT") + valueUSDT = trade.matchedQuantity * trade.matchedPrice + valueIRT = trade.matchedQuantity * baseIRTRate + } + + userVolumePersister.update( + trade.makerUuid, + base, + today, + trade.matchedQuantity, + valueUSDT, + valueIRT + ) + userVolumePersister.update( + trade.takerUuid, + base, + today, + trade.matchedQuantity, + valueUSDT, + valueIRT + ) + logger.info("Trade volume updated") + } + private fun createFinalizeOrderFinancialAction( parentTransferAction: FinancialAction?, order: Order, diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt index 7a028a39d..96ded5fde 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/CurrencyRatePersister.kt @@ -5,4 +5,6 @@ import java.math.BigDecimal interface CurrencyRatePersister { suspend fun updateRate(base: String, quote: String, rate: BigDecimal) + + suspend fun getRate(base: String, quote: String): BigDecimal } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt new file mode 100644 index 000000000..2d9864fdd --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt @@ -0,0 +1,16 @@ +package co.nilin.opex.accountant.core.spi + +import java.math.BigDecimal +import java.time.LocalDate + +interface UserVolumePersister { + + suspend fun update( + userId: String, + currency: String, + date: LocalDate, + volume: BigDecimal, + valueUSDT: BigDecimal, + valueIRT: BigDecimal + ) +} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt index 994477f43..76ab3e390 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/CurrencyRateRepository.kt @@ -21,8 +21,8 @@ interface CurrencyRateRepository : ReactiveCrudRepository - @Query("select * from currency_rate where base = :base and quote = :quote and source = :source") - fun findByBaseAndQuote(base: String, quote: String): Mono + @Query("select rate from currency_rate where base = :base and quote = :quote and source = :source") + fun findByBaseAndQuote(base: String, quote: String): Mono @Query("select * from currency_rate where quote = :quote and source = :source") fun findAllByQuote(quote: String): Flux diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt index 0a5a7fde1..cbeb0dd06 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/CurrencyRatePersisterImpl.kt @@ -12,4 +12,8 @@ class CurrencyRatePersisterImpl(private val repository: CurrencyRateRepository) override suspend fun updateRate(base: String, quote: String, rate: BigDecimal) { repository.createOrUpdate(base, quote, rate).awaitSingleOrNull() } + + override suspend fun getRate(base: String, quote: String): BigDecimal { + return repository.findByBaseAndQuote(base, quote).awaitSingleOrNull() ?: BigDecimal.ZERO + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt new file mode 100644 index 000000000..3dc269461 --- /dev/null +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt @@ -0,0 +1,23 @@ +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.spi.UserVolumePersister +import co.nilin.opex.accountant.ports.postgres.dao.UserTradeVolumeRepository +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.LocalDate + +@Component +class UserVolumePersisterImpl(private val repository: UserTradeVolumeRepository) : UserVolumePersister { + + override suspend fun update( + userId: String, + currency: String, + date: LocalDate, + volume: BigDecimal, + valueUSDT: BigDecimal, + valueIRT: BigDecimal + ) { + repository.insertOrUpdate(userId, currency, date, volume, valueUSDT, valueIRT).awaitSingleOrNull() + } +} \ No newline at end of file From 55809566c714ac6b729587e24bc1bd39c6611ea8 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 27 Aug 2025 12:37:09 +0330 Subject: [PATCH 08/12] Refactor fee calculation --- .../opex/accountant/app/config/AppConfig.kt | 20 ++--- .../app/controller/PairConfigController.kt | 28 +----- .../app/listener/KycLevelUpdatedListener.kt | 34 ------- .../opex/accountant/core/api/FeeCalculator.kt | 3 + .../accountant/core/model/PairFeeConfig.kt | 11 --- .../core/model/TotalAssetsSnapshot.kt | 11 +++ .../core/service/FeeCalculatorImpl.kt | 37 +++++++- .../core/service/OrderManagerImpl.kt | 36 ++++---- .../accountant/core/spi/PairConfigLoader.kt | 9 -- .../accountant/core/spi/UserLevelLoader.kt | 11 --- .../core/spi/UserVolumePersister.kt | 3 + .../opex/accountant/core/spi/WalletProxy.kt | 5 ++ .../accountant/core/utils/CacheManager.kt | 43 +++++++++ .../core/service/FeeCalculatorImplTest.kt | 19 +++- .../core/service/OrderManagerImplTest.kt | 32 ++----- .../core/service/TradeManagerImplTest.kt | 63 ++++++------- .../postgres/dao/PairFeeConfigRepository.kt | 25 ------ .../ports/postgres/dao/UserFeeRepository.kt | 12 --- .../postgres/dao/UserLevelMapperRepository.kt | 13 --- .../ports/postgres/dao/UserLevelRepository.kt | 17 ---- .../postgres/impl/PairConfigLoaderImpl.kt | 90 ------------------- .../postgres/impl/UserLevelLoaderImpl.kt | 46 ---------- .../postgres/impl/UserVolumePersisterImpl.kt | 5 ++ .../postgres/model/PairFeeConfigModel.kt | 15 ---- .../ports/postgres/model/UserFeeModel.kt | 14 --- .../postgres/model/UserLevelMapperModel.kt | 11 --- .../ports/postgres/model/UserLevelModel.kt | 7 -- .../src/main/resources/schema.sql | 43 ++------- .../ports/postgres/PairConfigLoaderTest.kt | 80 +---------------- .../opex/accountant/ports/postgres/Valid.kt | 9 -- .../walletproxy/proxy/WalletProxyImpl.kt | 14 +++ .../opex/wallet/app/config/SecurityConfig.kt | 1 + .../app/controller/WalletStatController.kt | 19 ++-- .../wallet/core/model/TotalAssetsSnapshot.kt | 2 +- .../core/spi/TotalAssetsSnapshotManager.kt | 9 +- .../dao/TotalAssetsSnapshotRepository.kt | 24 ++--- .../postgres/impl/TotalAssetsSnapshotImpl.kt | 11 +-- .../model/TotalAssetsSnapshotModel.kt | 2 +- .../wallet/ports/postgres/util/Convertor.kt | 2 +- .../src/main/resources/schema.sql | 3 +- 40 files changed, 256 insertions(+), 583 deletions(-) delete mode 100644 accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt delete mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt delete mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/utils/CacheManager.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt delete 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 8b4e26e95..a621b0ef2 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 @@ -9,7 +9,10 @@ import co.nilin.opex.accountant.core.service.FinancialActionJobManagerImpl import co.nilin.opex.accountant.core.service.OrderManagerImpl import co.nilin.opex.accountant.core.service.TradeManagerImpl import co.nilin.opex.accountant.core.spi.* -import co.nilin.opex.accountant.ports.kafka.listener.consumer.* +import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener import co.nilin.opex.accountant.ports.kafka.listener.spi.FAResponseListener import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean @@ -36,7 +39,6 @@ class AppConfig { @Bean fun orderManager( pairConfigLoader: PairConfigLoader, - userLevelLoader: UserLevelLoader, financialActionPersister: FinancialActionPersister, financeActionLoader: FinancialActionLoader, orderPersister: OrderPersister, @@ -44,18 +46,17 @@ class AppConfig { tempEventRepublisher: TempEventRepublisher, richOrderPublisher: RichOrderPublisher, financialActionPublisher: FinancialActionPublisher, - jsonMapper: JsonMapper + feeCalculator: FeeCalculator ): OrderManager { return OrderManagerImpl( pairConfigLoader, - userLevelLoader, financialActionPersister, financeActionLoader, orderPersister, tempEventPersister, richOrderPublisher, financialActionPublisher, - jsonMapper + feeCalculator ) } @@ -69,7 +70,8 @@ class AppConfig { richOrderPublisher: RichOrderPublisher, feeCalculator: FeeCalculator, financialActionPublisher: FinancialActionPublisher, - jsonMapper: JsonMapper + currencyRatePersister: CurrencyRatePersister, + userVolumePersister: UserVolumePersister, ): TradeManager { return TradeManagerImpl( financeActionPersister, @@ -80,7 +82,8 @@ class AppConfig { richOrderPublisher, feeCalculator, financialActionPublisher, - jsonMapper + currencyRatePersister, + userVolumePersister ) } @@ -129,11 +132,8 @@ class AppConfig { fun configureEventListener( eventKafkaListener: EventKafkaListener, accountantEventListener: AccountantEventListener, - kycLevelUpdatedKafkaListener: KycLevelUpdatedKafkaListener, - kycLevelUpdatedEventListener: KycLevelUpdatedListener ) { eventKafkaListener.addListener(accountantEventListener) - kycLevelUpdatedKafkaListener.addEventListener(kycLevelUpdatedEventListener) } @Autowired diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt index 5049be287..a0dce60d2 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/PairConfigController.kt @@ -1,14 +1,14 @@ package co.nilin.opex.accountant.app.controller -import co.nilin.opex.accountant.app.data.PairFeeResponse import co.nilin.opex.accountant.core.model.FeeConfig import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig import co.nilin.opex.accountant.core.spi.FeeConfigService import co.nilin.opex.accountant.core.spi.PairConfigLoader -import co.nilin.opex.common.OpexError import co.nilin.opex.matching.engine.core.model.OrderDirection -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/config") @@ -17,15 +17,6 @@ class PairConfigController( private val feeConfigService: FeeConfigService ) { - @GetMapping("/{pair}/fee/{direction}-{userLevel}") - suspend fun fetchPairFeeConfig( - @PathVariable("pair") pair: String, - @PathVariable("direction") direction: OrderDirection, - @PathVariable("userLevel") level: String - ): PairFeeConfig { - return pairConfigLoader.load(pair, direction, level) - } - @GetMapping("/{pair}/{direction}") suspend fun fetchPairConfig( @PathVariable("pair") pair: String, @@ -43,15 +34,4 @@ class PairConfigController( suspend fun getFeeConfigs(): List { return feeConfigService.loadFeeConfigs() } - - @GetMapping("/fee/{pair}") - suspend fun getFeeConfig( - @PathVariable pair: String, - @RequestParam(required = false) direction: OrderDirection?, - @RequestParam(required = false) userLevel: String? - ): PairFeeResponse { - val fee = pairConfigLoader.loadPairFeeConfigs(pair, direction ?: OrderDirection.BID, userLevel ?: "*") - ?: throw OpexError.PairFeeNotFound.exception() - return PairFeeResponse(fee.pairConfig.pair, fee.direction, fee.userLevel, fee.makerFee, fee.takerFee) - } } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt deleted file mode 100644 index 98ae6147b..000000000 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/KycLevelUpdatedListener.kt +++ /dev/null @@ -1,34 +0,0 @@ -package co.nilin.opex.accountant.app.listener - -import co.nilin.opex.accountant.core.inout.KycLevelUpdatedEvent -import co.nilin.opex.accountant.ports.kafka.listener.spi.KycLevelUpdatedEventListener -import co.nilin.opex.accountant.ports.postgres.impl.UserLevelLoaderImpl -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component - -@Component -class KycLevelUpdatedListener(val userLevelLoaderImpl: UserLevelLoaderImpl) : KycLevelUpdatedEventListener { - - private val logger = LoggerFactory.getLogger(KycLevelUpdatedListener::class.java) - val scope = CoroutineScope(Dispatchers.IO) - override fun id(): String { - return "KycLevelUpdatedListener" - } - - override fun onEvent( - event: KycLevelUpdatedEvent, - partition: Int, offset: Long, timestamp: Long, eventId: String - ) { - logger.info("==========================================================================") - logger.info("Incoming UserLevelUpdated event: $event") - logger.info("==========================================================================") - scope.launch { - userLevelLoaderImpl.update(event.userId, event.kycLevel) - } - } - - -} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt index 97adf6933..9c2f3ef32 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt @@ -1,5 +1,6 @@ package co.nilin.opex.accountant.core.api +import co.nilin.opex.accountant.core.model.FeeConfig import co.nilin.opex.accountant.core.model.FeeFinancialActions import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order @@ -7,6 +8,8 @@ import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent interface FeeCalculator { + suspend fun getUserFee(uuid: String): FeeConfig + suspend fun createFeeActions( trade: TradeEvent, makerOrder: Order, diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt deleted file mode 100644 index 052e6f2f8..000000000 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/PairFeeConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package co.nilin.opex.accountant.core.model - -import java.math.BigDecimal - -class PairFeeConfig( - val pairConfig: PairConfig, - val direction: String, - val userLevel: String, - val makerFee: BigDecimal, - val takerFee: BigDecimal -) \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt new file mode 100644 index 000000000..ef38bbefc --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/TotalAssetsSnapshot.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.accountant.core.model + +import java.math.BigDecimal +import java.time.LocalDateTime + +data class TotalAssetsSnapshot( + val uuid: String, + val totalUSDT: BigDecimal, + val totalIRT: BigDecimal, + val snapshotDate: LocalDateTime, +) \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt index 7e22d75ab..310027d18 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -2,21 +2,56 @@ package co.nilin.opex.accountant.core.service import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.model.* +import co.nilin.opex.accountant.core.spi.FeeConfigService import co.nilin.opex.accountant.core.spi.JsonMapper +import co.nilin.opex.accountant.core.spi.UserVolumePersister +import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.accountant.core.utils.CacheManager import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component -import java.time.LocalDateTime +import java.math.BigDecimal +import java.time.* +import java.util.concurrent.TimeUnit @Component class FeeCalculatorImpl( + private val walletProxy: WalletProxy, + private val feeConfigService: FeeConfigService, + private val userVolumePersister: UserVolumePersister, + private val cacheManager: CacheManager, @Value("\${app.address}") private val platformAddress: String, private val jsonMapper: JsonMapper ) : FeeCalculator { private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java) + private fun ttlUntilNextDay1amMillis(zone: ZoneId = ZoneId.of("GMT+03:30")): Long { + val now = ZonedDateTime.now(zone) + val target = now.toLocalDate().plusDays(1).atTime(1, 0).atZone(zone) + val ttl = Duration.between(now, target).toMillis() + return if (ttl > 0) ttl else Duration.ofHours(1).toMillis() + } + + override suspend fun getUserFee(uuid: String): FeeConfig { + val cached = cacheManager.get(uuid) + if (cached != null) { + return cached + } + + val totalAssets = walletProxy.getUserTotalAssets(uuid) + val userVolumeData = userVolumePersister.getUserVolumeData(uuid, LocalDate.now().minusMonths(1L)) + + val feeConfig = feeConfigService.loadMatchingFeeConfig( + totalAssets?.totalUSDT ?: BigDecimal.ZERO, + userVolumeData?.valueUSDT ?: BigDecimal.ZERO + ) + val ttl = ttlUntilNextDay1amMillis() + cacheManager.put(uuid, feeConfig, ttl, TimeUnit.MILLISECONDS) + return feeConfig + } + override suspend fun createFeeActions( trade: TradeEvent, makerOrder: Order, 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 7a0067599..34bf51f4b 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt @@ -1,5 +1,6 @@ package co.nilin.opex.accountant.core.service +import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.api.OrderManager import co.nilin.opex.accountant.core.inout.OrderStatus import co.nilin.opex.accountant.core.inout.RichOrder @@ -15,13 +16,13 @@ import java.time.LocalDateTime open class OrderManagerImpl( private val pairConfigLoader: PairConfigLoader, - private val userLevelLoader: UserLevelLoader, private val financialActionPersister: FinancialActionPersister, private val financeActionLoader: FinancialActionLoader, private val orderPersister: OrderPersister, private val tempEventPersister: TempEventPersister, private val richOrderPublisher: RichOrderPublisher, - private val financialActionPublisher: FinancialActionPublisher + private val financialActionPublisher: FinancialActionPublisher, + private val feeCalculator: FeeCalculator ) : OrderManager { @Transactional @@ -35,14 +36,13 @@ open class OrderManagerImpl( submitOrderEvent.pair.rightSideName } - val level = userLevelLoader.load(submitOrderEvent.uuid) - val pairFeeConfig = pairConfigLoader.load( + val pairConfig = pairConfigLoader.load( submitOrderEvent.pair.toString(), submitOrderEvent.direction, - level ) - val makerFee = pairFeeConfig.makerFee * BigDecimal.ONE //user level formula - val takerFee = pairFeeConfig.takerFee * BigDecimal.ONE //user level formula + val fee = feeCalculator.getUserFee(submitOrderEvent.uuid) + val makerFee = fee.makerFee + val takerFee = fee.takerFee //create fa for transfer uuid symbol main wallet to uuid symbol exchange wallet /* @@ -51,11 +51,11 @@ open class OrderManagerImpl( */ val amount = if (submitOrderEvent.direction == OrderDirection.ASK) { - BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction) + BigDecimal(submitOrderEvent.quantity).multiply(pairConfig.leftSideFraction) } else { - BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction) + BigDecimal(submitOrderEvent.quantity).multiply(pairConfig.leftSideFraction) .multiply(submitOrderEvent.price.toBigDecimal()) - .multiply(pairFeeConfig.pairConfig.rightSideFraction) + .multiply(pairConfig.rightSideFraction) } //store order (ouid, uuid, fees, userlevel, pair, direction, price, quantity, filledQ, status, transfered) @@ -66,8 +66,8 @@ open class OrderManagerImpl( null, makerFee, takerFee, - pairFeeConfig.pairConfig.leftSideFraction, - pairFeeConfig.pairConfig.rightSideFraction, + pairConfig.leftSideFraction, + pairConfig.rightSideFraction, submitOrderEvent.uuid, submitOrderEvent.userLevel, submitOrderEvent.direction, @@ -77,10 +77,10 @@ open class OrderManagerImpl( submitOrderEvent.quantity, submitOrderEvent.quantity - submitOrderEvent.remainedQuantity, submitOrderEvent.price.toBigDecimal() - .multiply(pairFeeConfig.pairConfig.rightSideFraction), + .multiply(pairConfig.rightSideFraction), submitOrderEvent.quantity.toBigDecimal() - .multiply(pairFeeConfig.pairConfig.leftSideFraction), - BigDecimal(submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).multiply(pairFeeConfig.pairConfig.leftSideFraction), + .multiply(pairConfig.leftSideFraction), + BigDecimal(submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).multiply(pairConfig.leftSideFraction), amount, amount, OrderStatus.REQUESTED.code @@ -268,4 +268,10 @@ open class OrderManagerImpl( } } + private suspend fun getFee(uuid: String): BigDecimal { + + + return BigDecimal.ONE + } + } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt index 800a8fe4b..a6a1d1c67 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/PairConfigLoader.kt @@ -1,20 +1,11 @@ package co.nilin.opex.accountant.core.spi import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig import co.nilin.opex.matching.engine.core.model.OrderDirection interface PairConfigLoader { suspend fun loadPairConfigs(): List - suspend fun loadPairFeeConfigs(): List - - suspend fun loadPairFeeConfigs(direction: OrderDirection, userLevel: String): List - - suspend fun loadPairFeeConfigs(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig? - - suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig - suspend fun load(pair: String, direction: OrderDirection): PairConfig } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt deleted file mode 100644 index 1946ee4fd..000000000 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserLevelLoader.kt +++ /dev/null @@ -1,11 +0,0 @@ -package co.nilin.opex.accountant.core.spi - -import co.nilin.opex.accountant.core.model.KycLevel - -interface UserLevelLoader { - - suspend fun load(uuid: String): String - - suspend fun update(uuid: String, userLevel: KycLevel) - -} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt index 2d9864fdd..a9e53d6ce 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/UserVolumePersister.kt @@ -1,5 +1,6 @@ package co.nilin.opex.accountant.core.spi +import co.nilin.opex.accountant.core.inout.UserTotalVolumeValue import java.math.BigDecimal import java.time.LocalDate @@ -13,4 +14,6 @@ interface UserVolumePersister { valueUSDT: BigDecimal, valueIRT: BigDecimal ) + + suspend fun getUserVolumeData(uuid: String, startDate: LocalDate): UserTotalVolumeValue? } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WalletProxy.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/WalletProxy.kt index 59bdce3fe..6da17ef2b 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.model.TotalAssetsSnapshot import co.nilin.opex.accountant.core.model.WalletType import java.math.BigDecimal @@ -18,4 +19,8 @@ interface WalletProxy { ) suspend fun canFulfil(symbol: String, walletType: WalletType, uuid: String, amount: BigDecimal): Boolean + + suspend fun getUserTotalAssets( + uuid: String + ): TotalAssetsSnapshot? } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/utils/CacheManager.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/utils/CacheManager.kt new file mode 100644 index 000000000..2004a5dfc --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/utils/CacheManager.kt @@ -0,0 +1,43 @@ +package co.nilin.opex.accountant.core.utils + +import org.springframework.stereotype.Component +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +@Component +class CacheManager { + + private val cacheMap = ConcurrentHashMap>() + + data class CacheEntry( + val value: T, + val timestamp: Long + ) + + fun put(key: K, value: V, expirationTime: Long, timeUnit: TimeUnit) { + val expirationMillis = timeUnit.toMillis(expirationTime) + cacheMap[key] = CacheEntry(value, System.currentTimeMillis() + expirationMillis) + } + + fun get(key: K): V? { + val entry = cacheMap[key] + return if (entry != null && !isExpired(entry)) { + entry.value + } else { + cacheMap.remove(key) + null + } + } + + private fun isExpired(entry: CacheEntry): Boolean { + return System.currentTimeMillis() > entry.timestamp + } + + fun remove(key: K) { + cacheMap.remove(key) + } + + fun clear() { + cacheMap.clear() + } +} \ No newline at end of file diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt index 8add7dac4..5ef7cbd6d 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt @@ -1,17 +1,34 @@ package co.nilin.opex.accountant.core.service +import co.nilin.opex.accountant.core.model.FeeConfig import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.FinancialActionCategory import co.nilin.opex.accountant.core.model.WalletType +import co.nilin.opex.accountant.core.spi.FeeConfigService +import co.nilin.opex.accountant.core.spi.UserVolumePersister +import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.accountant.core.utils.CacheManager import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test internal class FeeCalculatorImplTest { + private val walletProxy = mockk() + private val feeConfigService = mockk() + private val userVolumePersister = mockk() + private val cacheManager = mockk>() private val receiverAddress = "0x0" - private val feeCalculator = FeeCalculatorImpl(receiverAddress, JsonMapperTestImpl()) + private val feeCalculator = FeeCalculatorImpl( + walletProxy, + feeConfigService, + userVolumePersister, + cacheManager, + receiverAddress, + JsonMapperTestImpl() + ) @Test fun givenTradeEventsAndOrders_whenFeeCalculated_feeActionsNotNull(): Unit = runBlocking { diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt index 24d1c05d6..841387adf 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImplTest.kt @@ -1,7 +1,11 @@ package co.nilin.opex.accountant.core.service +import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.inout.OrderStatus -import co.nilin.opex.accountant.core.model.* +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionCategory +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.WalletType import co.nilin.opex.accountant.core.spi.* import co.nilin.opex.matching.engine.core.eventh.events.CancelOrderEvent import co.nilin.opex.matching.engine.core.eventh.events.CreateOrderEvent @@ -30,19 +34,18 @@ internal class OrderManagerImplTest { private val tempEventPersister = mockk() private val pairConfigLoader = mockk() private val richOrderPublisher = mockk() - private val userLevelLoader = mockk() private val financialActionPublisher = mockk() + private val feeCalculator = mockk() private val orderManager = OrderManagerImpl( pairConfigLoader, - userLevelLoader, financialActionPersister, financialActionLoader, orderPersister, tempEventPersister, richOrderPublisher, financialActionPublisher, - JsonMapperTestImpl() + feeCalculator ) init { @@ -54,7 +57,6 @@ internal class OrderManagerImplTest { coEvery { financialActionPersister.persist(any()) } returnsArgument (0) coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit - coEvery { userLevelLoader.load(any()) } returns "*" coEvery { financialActionPublisher.publish(any()) } returns Unit } @@ -73,16 +75,6 @@ internal class OrderManagerImplTest { "ouid", "uuid", null, pair, 30, 60, 0, OrderDirection.ASK, MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - coEvery { - pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any()) - } returns PairFeeConfig( - pairConfig, - submitOrderEvent.direction.toString(), - "", - BigDecimal.valueOf(0.1), - BigDecimal.valueOf(0.12) - ) - coEvery { financialActionPersister.persist(any()) } returnsArgument (0) //when @@ -130,16 +122,6 @@ internal class OrderManagerImplTest { "ouid", "uuid", null, pair, 35, 14, 0, OrderDirection.BID, MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - coEvery { - pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any()) - } returns PairFeeConfig( - pairConfig, - submitOrderEvent.direction.toString(), - "", - BigDecimal.valueOf(0.08), - BigDecimal.valueOf(0.1) - ) - coEvery { financialActionPersister.persist(any()) } returnsArgument (0) //when 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 f7059210f..de9150a1f 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt @@ -1,7 +1,9 @@ package co.nilin.opex.accountant.core.service +import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.model.* import co.nilin.opex.accountant.core.spi.* +import co.nilin.opex.accountant.core.utils.CacheManager import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent import co.nilin.opex.matching.engine.core.model.MatchConstraint @@ -24,21 +26,25 @@ internal class TradeManagerImplTest { private val tempEventPersister = mockk() private val richOrderPublisher = mockk() private val richTradePublisher = mockk() - private val userLevelLoader = mockk() private val financialActionPublisher = mockk() + private val currencyRatePersister = mockk() + private val userVolumePersister = mockk() + private val feeCalculator = mockk() + private val walletProxy = mockk() + private val feeConfigService = mockk() + private val cacheManager = mockk>() private val jsonMapper = JsonMapperTestImpl() private val orderManager = OrderManagerImpl( pairConfigLoader, - userLevelLoader, financialActionPersister, financeActionLoader, orderPersister, tempEventPersister, richOrderPublisher, financialActionPublisher, - jsonMapper + feeCalculator ) private val tradeManager = TradeManagerImpl( @@ -48,8 +54,17 @@ internal class TradeManagerImplTest { tempEventPersister, richTradePublisher, richOrderPublisher, - FeeCalculatorImpl("0x0", jsonMapper), + FeeCalculatorImpl( + walletProxy, + feeConfigService, + userVolumePersister, + cacheManager, + "0x0", + JsonMapperTestImpl() + ), financialActionPublisher, + currencyRatePersister, + userVolumePersister, ) init { @@ -58,7 +73,6 @@ internal class TradeManagerImplTest { coEvery { financeActionLoader.findLast(any(), any()) } returns null coEvery { richOrderPublisher.publish(any()) } returns Unit coEvery { richTradePublisher.publish(any()) } returns Unit - coEvery { userLevelLoader.load(any()) } returns "*" coEvery { financialActionPublisher.publish(any()) } returns Unit coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit @@ -87,7 +101,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -102,7 +116,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -141,7 +155,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -156,7 +170,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -190,7 +204,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -205,7 +219,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -244,7 +258,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -259,7 +273,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -302,29 +316,18 @@ internal class TradeManagerImplTest { } private suspend fun prepareOrder( - pair: Pair, - pairConfig: PairConfig, submitOrderEvent: SubmitOrderEvent, makerFee: BigDecimal, takerFee: BigDecimal ) { - coEvery { - pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, any()) - } returns PairFeeConfig( - pairConfig, - submitOrderEvent.direction.toString(), - "", - makerFee, - takerFee - ) coEvery { financialActionPersister.persist(any()) } returnsArgument (0) val financialActions = orderManager.handleRequestOrder(submitOrderEvent) - val orderPairFeeConfig = - pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "") - val orderMakerFee = orderPairFeeConfig.makerFee * BigDecimal.ONE //user level formula - val orderTakerFee = orderPairFeeConfig.takerFee * BigDecimal.ONE //user level formula + val pairConfig = + pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction) + val orderMakerFee = makerFee + val orderTakerFee = takerFee coEvery { orderPersister.load(submitOrderEvent.ouid) } returns Order( submitOrderEvent.pair.toString(), @@ -332,8 +335,8 @@ internal class TradeManagerImplTest { null, orderMakerFee, orderTakerFee, - orderPairFeeConfig.pairConfig.leftSideFraction, - orderPairFeeConfig.pairConfig.rightSideFraction, + pairConfig.leftSideFraction, + pairConfig.rightSideFraction, submitOrderEvent.uuid, submitOrderEvent.userLevel, submitOrderEvent.direction, diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt deleted file mode 100644 index 105bdc042..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairFeeConfigRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.dao - -import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel -import co.nilin.opex.matching.engine.core.model.OrderDirection -import org.springframework.data.r2dbc.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono - -@Repository -interface PairFeeConfigRepository : ReactiveCrudRepository { - - @Query("select * from pair_fee_config where pair_config_id = :pair and direction = :direction and user_level = :userLevel") - fun findByPairAndDirectionAndUserLevel( - @Param("pair") pair: String, - @Param("direction") direction: OrderDirection, - @Param("userLevel") userLevel: String - ): Mono - - @Query("select * from pair_fee_config where direction = :direction and user_level = :userLevel") - fun findByDirectionAndUserLevel(direction: OrderDirection, userLevel: String): Flux - -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt deleted file mode 100644 index f88aa566a..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserFeeRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.dao - -import co.nilin.opex.accountant.ports.postgres.model.UserFeeModel -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Mono - -@Repository -interface UserFeeRepository : ReactiveCrudRepository { - - fun findByUuidAndQuoteSymbol(uuid: String, quoteSymbol: 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/UserLevelMapperRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt deleted file mode 100644 index 9156013b6..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelMapperRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.dao - -import co.nilin.opex.accountant.ports.postgres.model.UserLevelMapperModel -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Mono - -@Repository -interface UserLevelMapperRepository : ReactiveCrudRepository { - - fun findByUuid(uuid: String): Mono - -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt deleted file mode 100644 index 9bf546613..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/UserLevelRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.dao - -import co.nilin.opex.accountant.ports.postgres.model.UserLevelModel -import org.springframework.data.r2dbc.repository.Query -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Mono - -@Repository -interface UserLevelRepository : ReactiveCrudRepository { - - fun findByLevel(level: String): Mono - - @Query("insert into user_level (level) values (:level) on conflict do nothing") - fun insert(level: String): Mono - -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt index 78487926e..5fe137ca4 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt @@ -1,24 +1,17 @@ package co.nilin.opex.accountant.ports.postgres.impl import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig import co.nilin.opex.accountant.core.spi.PairConfigLoader import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository -import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository import co.nilin.opex.accountant.ports.postgres.model.PairConfigModel -import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel import co.nilin.opex.common.OpexError import co.nilin.opex.matching.engine.core.model.OrderDirection import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitFirstOrNull -import kotlinx.coroutines.reactive.awaitSingle -import kotlinx.coroutines.reactor.awaitSingleOrNull import org.springframework.stereotype.Component @Component class PairConfigLoaderImpl( private val pairConfigRepository: PairConfigRepository, - private val pairFeeConfigRepository: PairFeeConfigRepository ) : PairConfigLoader { override suspend fun loadPairConfigs(): List { @@ -28,89 +21,6 @@ class PairConfigLoaderImpl( .map { it.asPairConfig() } } - override suspend fun loadPairFeeConfigs(): List { - return pairFeeConfigRepository.findAll() - .collectList() - .awaitFirstOrElse { emptyList() } - .map { - val pairConfig = pairConfigRepository.findById(it.pairConfigId).awaitSingle().asPairConfig() - PairFeeConfig(pairConfig, it.direction, it.userLevel, it.makerFee, it.takerFee) - } - } - - override suspend fun loadPairFeeConfigs(direction: OrderDirection, userLevel: String): List { - return pairFeeConfigRepository.findByDirectionAndUserLevel(direction, userLevel) - .collectList() - .awaitFirstOrElse { emptyList() } - .map { - val pairConfig = pairConfigRepository.findById(it.pairConfigId).awaitSingle().asPairConfig() - PairFeeConfig( - pairConfig, - it.direction, - it.userLevel, - it.makerFee, - it.takerFee - ) - } - } - - override suspend fun loadPairFeeConfigs( - pair: String, - direction: OrderDirection, - userLevel: String - ): PairFeeConfig? { - val fee = pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(pair, direction, userLevel) - .awaitSingleOrNull() ?: return null - val pairConfig = pairConfigRepository.findById(fee.pairConfigId).awaitSingle().asPairConfig() - return PairFeeConfig( - pairConfig, - fee.direction, - fee.userLevel, - fee.makerFee, - fee.takerFee - ) - } - - override suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { - val pairConfig = pairConfigRepository.findById(pair).awaitFirstOrElse { - throw OpexError.InvalidPair.messageFormattedException(pair) - } - - var pairFeeConfig: PairFeeConfigModel? - if (userLevel.isEmpty()) { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, "*") - .awaitFirstOrElse { - throw OpexError.InvalidPair.messageFormattedException(pair) - } - } else { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, userLevel) - .awaitFirstOrNull() - if (pairFeeConfig == null) { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, "*") - .awaitFirstOrElse { - throw OpexError.InvalidPair.messageFormattedException(pair) - } - } - } - - return PairFeeConfig( - PairConfig( - pair, - pairConfig.leftSideWalletSymbol, - pairConfig.rightSideWalletSymbol, - pairConfig.leftSideFraction, - pairConfig.rightSideFraction - ), - pairFeeConfig!!.direction, - pairFeeConfig.userLevel, - pairFeeConfig.makerFee, - pairFeeConfig.takerFee - ) - } - override suspend fun load(pair: String, direction: OrderDirection): PairConfig { return pairConfigRepository .findById(pair).awaitFirstOrElse { diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt deleted file mode 100644 index 4ecd0b13f..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserLevelLoaderImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.KycLevel -import co.nilin.opex.accountant.core.spi.UserLevelLoader -import co.nilin.opex.accountant.ports.postgres.dao.UserLevelMapperRepository -import co.nilin.opex.accountant.ports.postgres.dao.UserLevelRepository -import co.nilin.opex.accountant.ports.postgres.model.UserLevelMapperModel -import kotlinx.coroutines.reactor.awaitSingleOrNull -import org.springframework.stereotype.Component - -@Component -class UserLevelLoaderImpl( - private val userLevelMapperRepository: UserLevelMapperRepository, - private val userLevelRepository: UserLevelRepository -) : UserLevelLoader { - - override suspend fun load(uuid: String): String { - val mapper = userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull() - return mapper?.userLevel ?: "*" - } - - override suspend fun update(uuid: String, userLevel: KycLevel) { - - userLevelRepository.findByLevel(userLevel.name).awaitSingleOrNull()?.let { - userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull() - ?.let { - userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name)) - .awaitSingleOrNull() - } - ?: run { - userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull() - } - } ?: run { - userLevelRepository.insert(userLevel.name).awaitSingleOrNull() - userLevelMapperRepository.findByUuid(uuid).awaitSingleOrNull() - ?.let { - userLevelMapperRepository.save(UserLevelMapperModel(it.id, it.uuid, userLevel.name)) - .awaitSingleOrNull() - } - ?: run { - userLevelMapperRepository.save(UserLevelMapperModel(null, uuid, userLevel.name)).awaitSingleOrNull() - } - - } - } -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt index 3dc269461..5a02f5a2b 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/UserVolumePersisterImpl.kt @@ -1,5 +1,6 @@ package co.nilin.opex.accountant.ports.postgres.impl +import co.nilin.opex.accountant.core.inout.UserTotalVolumeValue import co.nilin.opex.accountant.core.spi.UserVolumePersister import co.nilin.opex.accountant.ports.postgres.dao.UserTradeVolumeRepository import kotlinx.coroutines.reactor.awaitSingleOrNull @@ -20,4 +21,8 @@ class UserVolumePersisterImpl(private val repository: UserTradeVolumeRepository) ) { repository.insertOrUpdate(userId, currency, date, volume, valueUSDT, valueIRT).awaitSingleOrNull() } + + override suspend fun getUserVolumeData(uuid: String, startDate: LocalDate): UserTotalVolumeValue? { + return repository.findTotalValueByUserAndAndDateAfter(uuid, startDate).awaitSingleOrNull() + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt deleted file mode 100644 index cd46a2abc..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairFeeConfigModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table -import java.math.BigDecimal - -@Table("pair_fee_config") -data class PairFeeConfigModel( - val id: Long?, - @Column("pair_config_id") val pairConfigId: String, - @Column("direction") val direction: String, - @Column("user_level") val userLevel: String, - @Column("maker_fee") val makerFee: BigDecimal, - @Column("taker_fee") val takerFee: BigDecimal -) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt deleted file mode 100644 index abed413cf..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserFeeModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Table -import java.math.BigDecimal - -@Table("user_fee") -data class UserFeeModel( - val uuid: String, - val quoteSymbol: String, - val makerFee: BigDecimal, - val takerFee: BigDecimal, - @Id val id: Long? = null -) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt deleted file mode 100644 index af4c5512e..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelMapperModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Table - -@Table("user_level_mapper") -data class UserLevelMapperModel( - @Id val id: Long?, - val uuid: String, - val userLevel: String -) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt deleted file mode 100644 index 2e9af91b0..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/UserLevelModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Table - -@Table("user_level") -data class UserLevelModel(@Id val level: String) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql index b3d4defb5..02bf568eb 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 @@ -84,29 +84,6 @@ CREATE TABLE IF NOT EXISTS pair_config 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 -( - id SERIAL PRIMARY KEY, - pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), - direction VARCHAR(36) NOT NULL, - user_level VARCHAR(36) NOT NULL REFERENCES user_level (level), - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - UNIQUE (direction, user_level, pair_config_id) -); - -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, @@ -116,16 +93,6 @@ CREATE TABLE IF NOT EXISTS temp_events event_date TIMESTAMP NOT NULL ); -CREATE TABLE IF NOT EXISTS user_fee -( - id SERIAL PRIMARY KEY, - uuid VARCHAR(32) NOT NULL UNIQUE, - quote_symbol VARCHAR(10) NOT NULL, - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_user_fee_quote_symbol ON user_fee (quote_symbol); - CREATE TABLE IF NOT EXISTS user_trade_volume ( id SERIAL PRIMARY KEY, @@ -147,11 +114,6 @@ CREATE TABLE IF NOT EXISTS currency_rate UNIQUE (base, quote) ); -INSERT INTO user_fee (uuid, quote_symbol, maker_fee, taker_fee) -values ('DEFAULT', 'USDT', 0.0025, 0.005), - ('DEFAULT', 'IRT', 0.0025, 0.005) -ON CONFLICT DO NOTHING; - CREATE TABLE IF NOT EXISTS fee_config ( name VARCHAR(50) PRIMARY KEY, display_order INTEGER NOT NULL UNIQUE, @@ -164,4 +126,9 @@ ON CONFLICT DO NOTHING; condition VARCHAR(10) NOT NULL ); + DROP TABLE IF EXISTS pair_fee_config; + DROP TABLE IF EXISTS user_fee; + DROP TABLE IF EXISTS user_level_mapper; + DROP TABLE IF EXISTS user_level; + COMMIT; diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt index 73da00a7f..37a22ed39 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/PairConfigLoaderTest.kt @@ -1,13 +1,11 @@ package co.nilin.opex.accountant.ports.postgres import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository -import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository import co.nilin.opex.accountant.ports.postgres.impl.PairConfigLoaderImpl import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.utility.error.data.OpexException import io.mockk.every import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -22,11 +20,7 @@ class PairConfigLoaderTest { every { findAll() } returns Flux.just(Valid.pairConfigModel, Valid.pairConfigModel) every { findById(any() as String) } returns Mono.just(Valid.pairConfigModel) } - private val pairFeeConfigRepository = mockk { - every { findAll() } returns Flux.just(Valid.pairFeeConfigModel, Valid.pairFeeConfigModel) - every { findByPairAndDirectionAndUserLevel(any(), any(), any()) } returns Mono.just(Valid.pairFeeConfigModel) - } - private val pairConfigLoader = PairConfigLoaderImpl(pairConfigRepository, pairFeeConfigRepository) + private val pairConfigLoader = PairConfigLoaderImpl(pairConfigRepository) @Test fun givenPairConfigs_whenListNotEmpty_resultIsNotEmptyAndValid(): Unit = runBlocking { @@ -41,84 +35,14 @@ class PairConfigLoaderTest { } } - @Test - fun givenPairFeeConfigs_whenListNotEmpty_resultIsNotEmptyAndValid(): Unit = runBlocking { - val configs = pairConfigLoader.loadPairFeeConfigs() - assertThat(configs.size).isEqualTo(2) - with(configs[1]) { - assertThat(pairConfig.pair).isEqualTo(Valid.pairConfig.pair) - assertThat(userLevel).isEqualTo(Valid.pairFeeConfigModel.userLevel) - assertThat(direction).isEqualTo(Valid.pairFeeConfigModel.direction) - assertThat(makerFee).isEqualTo(Valid.pairFeeConfigModel.makerFee) - assertThat(takerFee).isEqualTo(Valid.pairFeeConfigModel.takerFee) - } - } - @Test fun givenPairDirectionUserLevel_whenPairConfigNotFound_throwsException(): Unit = runBlocking { every { pairConfigRepository.findById(any() as String) } returns Mono.empty() assertThatThrownBy { - runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "*") } - }.isInstanceOf(OpexException::class.java) - } - - @Test - fun givenPairDirection_whenUserLevelEmpty_loadWithDefaultUserLevel(): Unit = runBlocking { - val pair = pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "") - assertThat(pair).isNotNull - verify(exactly = 1) { - pairFeeConfigRepository.findByPairAndDirectionAndUserLevel( - eq("BTC_USDT"), - eq(OrderDirection.BID), - eq("*") - ) - } - } - - @Test - fun givenPairDirection_whenPairFeeConfigNotFound_throwsException(): Unit = runBlocking { - every { - pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("*")) - } returns Mono.empty() - - assertThatThrownBy { - runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "") } + runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID) } }.isInstanceOf(OpexException::class.java) } - @Test - fun givenPairDirectionUserLevel_whenPairFeeConfigNotFound_loadWithDefaultUserLevel(): Unit = runBlocking { - every { - pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("1")) - } returns Mono.empty() - - val pair = pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "1") - assertThat(pair).isNotNull - verify(exactly = 1) { - pairFeeConfigRepository.findByPairAndDirectionAndUserLevel( - eq("BTC_USDT"), - eq(OrderDirection.BID), - eq("*") - ) - } - } - - @Test - fun givenPairDirectionUserLevel_whenPairFeeConfigNotFoundWithActualAndDefaultUserLevel_throwsException(): Unit = - runBlocking { - every { - pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("1")) - } returns Mono.empty() - - every { - pairFeeConfigRepository.findByPairAndDirectionAndUserLevel(any(), any(), eq("*")) - } returns Mono.empty() - - assertThatThrownBy { - runBlocking { pairConfigLoader.load("BTC_USDT", OrderDirection.BID, "1") } - }.isInstanceOf(OpexException::class.java) - } - @Test fun givenPairDirection_whenPairConfigNotFound_throwException(): Unit = runBlocking { every { pairConfigRepository.findById(any() as String) } returns Mono.empty() diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt index af2eb5f88..b96047449 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/test/kotlin/co/nilin/opex/accountant/ports/postgres/Valid.kt @@ -85,15 +85,6 @@ object Valid { 0.01.toBigDecimal() ) - val pairFeeConfigModel = PairFeeConfigModel( - 1, - "BTC_USDT", - "BID", - "1", - 0.01.toBigDecimal(), - 0.01.toBigDecimal() - ) - class TestCoreEvent(val leftSidePair: String, val rightSidePair: String) : CoreEvent(Pair(leftSidePair, rightSidePair), currentTime) diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt index c9ed17d3e..01be95214 100644 --- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt @@ -1,10 +1,12 @@ package co.nilin.opex.accountant.ports.walletproxy.proxy +import co.nilin.opex.accountant.core.model.TotalAssetsSnapshot import co.nilin.opex.accountant.core.model.WalletType import co.nilin.opex.accountant.core.spi.WalletProxy import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse import co.nilin.opex.accountant.ports.walletproxy.data.TransferResult import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrNull import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType import org.springframework.stereotype.Component @@ -56,4 +58,16 @@ class WalletProxyImpl( .awaitFirst() .result } + + override suspend fun getUserTotalAssets( + uuid: String, + ): TotalAssetsSnapshot? { + return webClient.get() + .uri("$walletBaseUrl/stats/total-assets/$uuid") + .header("Content-Type", "application/json") + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono() + .awaitFirstOrNull() + } } \ No newline at end of file diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt index ccb593ba6..1763eeb13 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt @@ -37,6 +37,7 @@ class SecurityConfig(private val webClient: WebClient) { .pathMatchers("/withdraw/**").authenticated() .pathMatchers("/transaction/**").authenticated() .pathMatchers("/admin/**").hasAuthority("ROLE_admin") + .pathMatchers("/stats/total-assets/**").permitAll() //TODO FIXME .pathMatchers("/stats/**").hasAuthority("ROLE_admin") .pathMatchers(HttpMethod.GET, "/currency/**").permitAll() .pathMatchers("/actuator/**").permitAll() diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletStatController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletStatController.kt index 4303035c9..648b3a70f 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletStatController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletStatController.kt @@ -3,16 +3,18 @@ package co.nilin.opex.wallet.app.controller import co.nilin.opex.wallet.core.inout.WalletData import co.nilin.opex.wallet.core.inout.WalletDataResponse import co.nilin.opex.wallet.core.inout.WalletTotal +import co.nilin.opex.wallet.core.model.TotalAssetsSnapshot import co.nilin.opex.wallet.core.model.WalletType +import co.nilin.opex.wallet.core.spi.TotalAssetsSnapshotManager import co.nilin.opex.wallet.core.spi.WalletDataManager -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/stats") -class WalletStatController(private val walletDataManager: WalletDataManager) { +class WalletStatController( + private val walletDataManager: WalletDataManager, + private val totalAssetsSnapshotManager: TotalAssetsSnapshotManager +) { @GetMapping("/wallets") suspend fun walletData( @@ -46,4 +48,11 @@ class WalletStatController(private val walletDataManager: WalletDataManager) { suspend fun userWalletTotal(): List? { return walletDataManager.findUserWalletsTotal() } + + @GetMapping("/total-assets/{uuid}") + suspend fun getWalletTotalAssetsSnapshot( + @PathVariable uuid: String, + ): TotalAssetsSnapshot? { + return totalAssetsSnapshotManager.getUserLastSnapshot(uuid) + } } \ No newline at end of file diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TotalAssetsSnapshot.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TotalAssetsSnapshot.kt index d382482c1..3e7dafeb3 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TotalAssetsSnapshot.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/model/TotalAssetsSnapshot.kt @@ -4,7 +4,7 @@ import java.math.BigDecimal import java.time.LocalDateTime data class TotalAssetsSnapshot( - val owner: Long, + val uuid: String, val totalUSDT: BigDecimal, val totalIRT: BigDecimal, val snapshotDate: LocalDateTime, diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TotalAssetsSnapshotManager.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TotalAssetsSnapshotManager.kt index 413f02e67..333e2c95c 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TotalAssetsSnapshotManager.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/TotalAssetsSnapshotManager.kt @@ -1,13 +1,10 @@ package co.nilin.opex.wallet.core.spi import co.nilin.opex.wallet.core.model.TotalAssetsSnapshot -import java.time.LocalDateTime interface TotalAssetsSnapshotManager { suspend fun createSnapshot() - suspend fun getByOwnerIdAndDate( - ownerId: Long, - fromDate: LocalDateTime? = null, - toDate: LocalDateTime? = null - ): List + suspend fun getUserLastSnapshot( + uuid: String + ): TotalAssetsSnapshot? } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/TotalAssetsSnapshotRepository.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/TotalAssetsSnapshotRepository.kt index 58fbe4a05..b84c11263 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/TotalAssetsSnapshotRepository.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/TotalAssetsSnapshotRepository.kt @@ -1,14 +1,11 @@ package co.nilin.opex.wallet.ports.postgres.dao -import co.nilin.opex.wallet.core.model.TotalAssetsSnapshot import co.nilin.opex.wallet.ports.postgres.model.TotalAssetsSnapshotModel import org.springframework.data.r2dbc.repository.Modifying import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.reactive.ReactiveCrudRepository import org.springframework.stereotype.Repository -import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import java.time.LocalDateTime @Repository interface TotalAssetsSnapshotRepository : ReactiveCrudRepository { @@ -17,7 +14,7 @@ interface TotalAssetsSnapshotRepository : ReactiveCrudRepository 0 - GROUP BY w.owner + GROUP BY wo.uuid """ ) fun createSnapshotsDirectly(): Mono @@ -55,16 +53,12 @@ interface TotalAssetsSnapshotRepository : ReactiveCrudRepository :startTime) - and (:endTime is null or snapshot_date <= :endTime) + where uuid = :uuid order by id desc + limit 1 """ ) - fun findByOwnerIdAndSnapshotDate( - ownerId: Long, - startTime: LocalDateTime?, - endTime: LocalDateTime? - ): Flux - + fun findLastSnapshotByUuid( + uuid: String, + ): Mono } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TotalAssetsSnapshotImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TotalAssetsSnapshotImpl.kt index 852c47379..3ad6b1aa3 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TotalAssetsSnapshotImpl.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/TotalAssetsSnapshotImpl.kt @@ -9,7 +9,6 @@ import co.nilin.opex.wallet.ports.postgres.util.toTotalAssetsSnapshot import kotlinx.coroutines.reactive.awaitFirstOrNull import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import java.time.LocalDateTime @Service class TotalAssetsSnapshotImpl( @@ -31,11 +30,10 @@ class TotalAssetsSnapshotImpl( logger.info("Snapshot creation finished in {} ms", (end - start)) } - override suspend fun getByOwnerIdAndDate( - ownerId: Long, fromDate: LocalDateTime?, toDate: LocalDateTime? - ): List { - return totalAssetsSnapshotRepository.findByOwnerIdAndSnapshotDate(ownerId, fromDate, toDate).collectList() - .awaitFirstOrNull()?.map { it.toTotalAssetsSnapshot() } ?: emptyList() + override suspend fun getUserLastSnapshot( + uuid: String + ): TotalAssetsSnapshot? { + return totalAssetsSnapshotRepository.findLastSnapshotByUuid(uuid).awaitFirstOrNull()?.toTotalAssetsSnapshot() } suspend fun savePrices() { @@ -46,5 +44,4 @@ class TotalAssetsSnapshotImpl( ).awaitFirstOrNull() } } - } diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TotalAssetsSnapshotModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TotalAssetsSnapshotModel.kt index abec5015a..227608d91 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TotalAssetsSnapshotModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TotalAssetsSnapshotModel.kt @@ -10,7 +10,7 @@ import java.time.LocalDateTime data class TotalAssetsSnapshotModel( @Id val id: Long? = null, - val owner: Long, + val uuid: String, @Column("total_usdt") val totalUSDT: BigDecimal, @Column("total_irt") diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/Convertor.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/Convertor.kt index 41c9c410d..c3fd3b3cf 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/Convertor.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/Convertor.kt @@ -175,7 +175,7 @@ fun CurrencyModel.toCurrencyData(): CurrencyData { fun TotalAssetsSnapshotModel.toTotalAssetsSnapshot(): TotalAssetsSnapshot { return TotalAssetsSnapshot( - owner, + uuid, totalUSDT, totalIRT, snapshotDate diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql index f7868864c..896b55423 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql @@ -546,11 +546,12 @@ $$; CREATE TABLE IF NOT EXISTS total_assets_snapshot ( id SERIAL PRIMARY KEY, - owner INTEGER REFERENCES wallet_owner (id), + uuid VARCHAR(36) NOT NULL, total_usdt DECIMAL NOT NULL, total_irt DECIMAL NOT NULL, snapshot_date TIMESTAMP NOT NULL ); +CREATE INDEX IF NOT EXISTS idx_total_assets_snapshot_uuid ON total_assets_snapshot (uuid); CREATE TABLE IF NOT EXISTS price ( From 6c68a99dd960cd9140d6cf2bea3090ba93288610 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 27 Aug 2025 13:11:22 +0330 Subject: [PATCH 09/12] Update TradeManagerImplTest.kt --- .../core/service/TradeManagerImplTest.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) 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 de9150a1f..fcc7031f8 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 @@ -101,7 +101,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -116,7 +116,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -155,7 +155,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -170,7 +170,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -204,7 +204,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -219,7 +219,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -258,7 +258,7 @@ internal class TradeManagerImplTest { MatchConstraint.GTC, OrderType.LIMIT_ORDER ) - prepareOrder(makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, BigDecimal.valueOf(0.1), BigDecimal.valueOf(0.12)) val takerSubmitOrderEvent = SubmitOrderEvent( "touid", @@ -273,7 +273,7 @@ internal class TradeManagerImplTest { OrderType.LIMIT_ORDER ) - prepareOrder(takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1)) val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent, 1) //when @@ -316,10 +316,23 @@ internal class TradeManagerImplTest { } private suspend fun prepareOrder( + pair: Pair, + pairConfig: PairConfig, submitOrderEvent: SubmitOrderEvent, makerFee: BigDecimal, takerFee: BigDecimal ) { + coEvery { + feeCalculator.getUserFee(submitOrderEvent.uuid) + } returns FeeConfig( + "Test", 1, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, makerFee, takerFee, + Condition.OR + ) + + coEvery { + pairConfigLoader.load(pair.toString(), submitOrderEvent.direction) + } returns pairConfig + coEvery { financialActionPersister.persist(any()) } returnsArgument (0) val financialActions = orderManager.handleRequestOrder(submitOrderEvent) From 961c8bd0105a602da9d90208340096ad0607abe3 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 27 Aug 2025 14:05:41 +0330 Subject: [PATCH 10/12] Fix test classes --- .../core/service/OrderManagerImplTest.kt | 24 +++++++++++++++---- .../core/service/TradeManagerImplTest.kt | 5 ++++ 2 files changed, 25 insertions(+), 4 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 841387adf..77a19f717 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 @@ -2,10 +2,7 @@ package co.nilin.opex.accountant.core.service import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.inout.OrderStatus -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionCategory -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.WalletType +import co.nilin.opex.accountant.core.model.* import co.nilin.opex.accountant.core.spi.* import co.nilin.opex.matching.engine.core.eventh.events.CancelOrderEvent import co.nilin.opex.matching.engine.core.eventh.events.CreateOrderEvent @@ -77,6 +74,15 @@ internal class OrderManagerImplTest { coEvery { financialActionPersister.persist(any()) } returnsArgument (0) + coEvery { + pairConfigLoader.load(pair.toString(), submitOrderEvent.direction) + } returns pairConfig + + coEvery { + feeCalculator.getUserFee(submitOrderEvent.uuid) + } returns FeeConfig( + "Test", 1, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1), Condition.OR + ) //when val financialActions = orderManager.handleRequestOrder(submitOrderEvent) @@ -124,6 +130,16 @@ internal class OrderManagerImplTest { coEvery { financialActionPersister.persist(any()) } returnsArgument (0) + coEvery { + pairConfigLoader.load(pair.toString(), submitOrderEvent.direction) + } returns pairConfig + + coEvery { + feeCalculator.getUserFee(submitOrderEvent.uuid) + } returns FeeConfig( + "Test", 1, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1), Condition.OR + ) + //when val financialActions = orderManager.handleRequestOrder(submitOrderEvent) diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt index fcc7031f8..8474f9d0d 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 @@ -11,10 +11,13 @@ import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.matching.engine.core.model.OrderType import co.nilin.opex.matching.engine.core.model.Pair import io.mockk.coEvery +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.mockito.kotlin.any import java.math.BigDecimal internal class TradeManagerImplTest { @@ -76,6 +79,8 @@ internal class TradeManagerImplTest { coEvery { financialActionPublisher.publish(any()) } returns Unit coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit + coEvery { currencyRatePersister.updateRate(any(), any(), any()) } just runs + coEvery { userVolumePersister.update(any(), any(), any(),any(),any(),any()) } just runs } @Test From 70b4d5744192a3ae27f579987507c7d3803047d3 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 27 Aug 2025 16:33:57 +0330 Subject: [PATCH 11/12] Some improvement --- .../opex/accountant/core/service/FeeCalculatorImpl.kt | 10 +++++----- .../co/nilin/opex/wallet/app/config/SecurityConfig.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt index 310027d18..9c3789131 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -27,11 +27,11 @@ class FeeCalculatorImpl( private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java) - private fun ttlUntilNextDay1amMillis(zone: ZoneId = ZoneId.of("GMT+03:30")): Long { + private fun remainingMillisUntil1AM(zone: ZoneId = ZoneId.of("GMT+03:30")): Long { val now = ZonedDateTime.now(zone) val target = now.toLocalDate().plusDays(1).atTime(1, 0).atZone(zone) - val ttl = Duration.between(now, target).toMillis() - return if (ttl > 0) ttl else Duration.ofHours(1).toMillis() + val remainingMillis = Duration.between(now, target).toMillis() + return if ( remainingMillis > 0) remainingMillis else Duration.ofHours(1).toMillis() } override suspend fun getUserFee(uuid: String): FeeConfig { @@ -47,8 +47,8 @@ class FeeCalculatorImpl( totalAssets?.totalUSDT ?: BigDecimal.ZERO, userVolumeData?.valueUSDT ?: BigDecimal.ZERO ) - val ttl = ttlUntilNextDay1amMillis() - cacheManager.put(uuid, feeConfig, ttl, TimeUnit.MILLISECONDS) + val remainingMillis = remainingMillisUntil1AM() + cacheManager.put(uuid, feeConfig, remainingMillis, TimeUnit.MILLISECONDS) return feeConfig } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt index 1763eeb13..3904d4740 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/SecurityConfig.kt @@ -37,7 +37,7 @@ class SecurityConfig(private val webClient: WebClient) { .pathMatchers("/withdraw/**").authenticated() .pathMatchers("/transaction/**").authenticated() .pathMatchers("/admin/**").hasAuthority("ROLE_admin") - .pathMatchers("/stats/total-assets/**").permitAll() //TODO FIXME + .pathMatchers("/stats/total-assets/**").permitAll() .pathMatchers("/stats/**").hasAuthority("ROLE_admin") .pathMatchers(HttpMethod.GET, "/currency/**").permitAll() .pathMatchers("/actuator/**").permitAll() From 9cf8c3572200db3bc8fe9bc3793756bb4a11c4e0 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 30 Aug 2025 19:59:49 +0330 Subject: [PATCH 12/12] Add user fee service --- .../app/controller/FeeController.kt | 27 +++++++++++++++++++ .../opex/accountant/core/api/FeeCalculator.kt | 4 +-- .../opex/accountant/core/model/UserFee.kt | 10 +++++++ .../core/service/FeeCalculatorImpl.kt | 12 ++++----- .../accountant/core/spi/FeeConfigService.kt | 3 ++- .../core/service/FeeCalculatorImplTest.kt | 4 +-- .../core/service/OrderManagerImplTest.kt | 8 +++--- .../core/service/TradeManagerImplTest.kt | 10 +++---- .../ports/postgres/dao/FeeConfigRepository.kt | 5 ++-- .../postgres/impl/FeeConfigServiceImpl.kt | 3 ++- .../co/nilin/opex/api/core/inout/UserFee.kt | 13 +++++++++ .../opex/api/core/spi/AccountantProxy.kt | 5 ++-- .../ports/opex/controller/MarketController.kt | 4 ++- .../opex/controller/UserDataController.kt | 17 ++++++++++-- .../ports/proxy/impl/AccountantProxyImpl.kt | 15 ++++++----- 15 files changed, 103 insertions(+), 37 deletions(-) create mode 100644 accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/FeeController.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/FeeController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/FeeController.kt new file mode 100644 index 000000000..426d5a834 --- /dev/null +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/FeeController.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.accountant.app.controller + +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.model.FeeConfig +import co.nilin.opex.accountant.core.model.UserFee +import co.nilin.opex.accountant.core.spi.FeeConfigService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/fee") +class FeeController( + private val feeConfigService: FeeConfigService, + private val feeCalculator: FeeCalculator, +) { + @GetMapping("/config") + suspend fun getFeeConfigs(): List { + return feeConfigService.loadFeeConfigs() + } + + @GetMapping("/{uuid}") + suspend fun getUserFee(@PathVariable uuid: String): UserFee { + return feeCalculator.getUserFee(uuid) + } +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt index 9c2f3ef32..205bf336b 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt @@ -1,14 +1,14 @@ package co.nilin.opex.accountant.core.api -import co.nilin.opex.accountant.core.model.FeeConfig import co.nilin.opex.accountant.core.model.FeeFinancialActions import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.accountant.core.model.UserFee import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent interface FeeCalculator { - suspend fun getUserFee(uuid: String): FeeConfig + suspend fun getUserFee(uuid: String): UserFee suspend fun createFeeActions( trade: TradeEvent, diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt new file mode 100644 index 000000000..fb6909d55 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/UserFee.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.accountant.core.model + +import java.math.BigDecimal + +data class UserFee( + val name: String, + val displayOrder: Int, + val makerFee: BigDecimal, + val takerFee: BigDecimal, +) diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt index 9c3789131..af73520b1 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -20,7 +20,7 @@ class FeeCalculatorImpl( private val walletProxy: WalletProxy, private val feeConfigService: FeeConfigService, private val userVolumePersister: UserVolumePersister, - private val cacheManager: CacheManager, + private val cacheManager: CacheManager, @Value("\${app.address}") private val platformAddress: String, private val jsonMapper: JsonMapper ) : FeeCalculator { @@ -30,11 +30,11 @@ class FeeCalculatorImpl( private fun remainingMillisUntil1AM(zone: ZoneId = ZoneId.of("GMT+03:30")): Long { val now = ZonedDateTime.now(zone) val target = now.toLocalDate().plusDays(1).atTime(1, 0).atZone(zone) - val remainingMillis = Duration.between(now, target).toMillis() - return if ( remainingMillis > 0) remainingMillis else Duration.ofHours(1).toMillis() + val remainingMillis = Duration.between(now, target).toMillis() + return if (remainingMillis > 0) remainingMillis else Duration.ofHours(1).toMillis() } - override suspend fun getUserFee(uuid: String): FeeConfig { + override suspend fun getUserFee(uuid: String): UserFee { val cached = cacheManager.get(uuid) if (cached != null) { return cached @@ -47,8 +47,8 @@ class FeeCalculatorImpl( totalAssets?.totalUSDT ?: BigDecimal.ZERO, userVolumeData?.valueUSDT ?: BigDecimal.ZERO ) - val remainingMillis = remainingMillisUntil1AM() - cacheManager.put(uuid, feeConfig, remainingMillis, TimeUnit.MILLISECONDS) + val remainingMillis = remainingMillisUntil1AM() + cacheManager.put(uuid, feeConfig, remainingMillis, TimeUnit.MILLISECONDS) return feeConfig } diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt index e61f45d05..e258cd7c9 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FeeConfigService.kt @@ -1,6 +1,7 @@ package co.nilin.opex.accountant.core.spi import co.nilin.opex.accountant.core.model.FeeConfig +import co.nilin.opex.accountant.core.model.UserFee import java.math.BigDecimal interface FeeConfigService { @@ -9,6 +10,6 @@ interface FeeConfigService { suspend fun saveFee(config: FeeConfig) suspend fun updateFee(config: FeeConfig) suspend fun loadFeeConfig(name: String): FeeConfig - suspend fun loadMatchingFeeConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): FeeConfig + suspend fun loadMatchingFeeConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): UserFee } \ No newline at end of file diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt index 5ef7cbd6d..f3ef40b87 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImplTest.kt @@ -1,8 +1,8 @@ package co.nilin.opex.accountant.core.service -import co.nilin.opex.accountant.core.model.FeeConfig import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.FinancialActionCategory +import co.nilin.opex.accountant.core.model.UserFee import co.nilin.opex.accountant.core.model.WalletType import co.nilin.opex.accountant.core.spi.FeeConfigService import co.nilin.opex.accountant.core.spi.UserVolumePersister @@ -18,7 +18,7 @@ internal class FeeCalculatorImplTest { private val walletProxy = mockk() private val feeConfigService = mockk() private val userVolumePersister = mockk() - private val cacheManager = mockk>() + private val cacheManager = mockk>() private val receiverAddress = "0x0" private val feeCalculator = FeeCalculatorImpl( 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 77a19f717..0abca2b78 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 @@ -80,8 +80,8 @@ internal class OrderManagerImplTest { coEvery { feeCalculator.getUserFee(submitOrderEvent.uuid) - } returns FeeConfig( - "Test", 1, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1), Condition.OR + } returns UserFee( + "Test", 1, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1) ) //when val financialActions = orderManager.handleRequestOrder(submitOrderEvent) @@ -136,8 +136,8 @@ internal class OrderManagerImplTest { coEvery { feeCalculator.getUserFee(submitOrderEvent.uuid) - } returns FeeConfig( - "Test", 1, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1), Condition.OR + } returns UserFee( + "Test", 1, BigDecimal.valueOf(0.08), BigDecimal.valueOf(0.1) ) //when 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 8474f9d0d..d0489dfdf 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 @@ -17,7 +17,6 @@ import io.mockk.runs import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.mockito.kotlin.any import java.math.BigDecimal internal class TradeManagerImplTest { @@ -35,7 +34,7 @@ internal class TradeManagerImplTest { private val feeCalculator = mockk() private val walletProxy = mockk() private val feeConfigService = mockk() - private val cacheManager = mockk>() + private val cacheManager = mockk>() private val jsonMapper = JsonMapperTestImpl() @@ -80,7 +79,7 @@ internal class TradeManagerImplTest { coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit coEvery { financialActionPersister.updateStatus(any(), any()) } returns Unit coEvery { currencyRatePersister.updateRate(any(), any(), any()) } just runs - coEvery { userVolumePersister.update(any(), any(), any(),any(),any(),any()) } just runs + coEvery { userVolumePersister.update(any(), any(), any(), any(), any(), any()) } just runs } @Test @@ -329,9 +328,8 @@ internal class TradeManagerImplTest { ) { coEvery { feeCalculator.getUserFee(submitOrderEvent.uuid) - } returns FeeConfig( - "Test", 1, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, makerFee, takerFee, - Condition.OR + } returns UserFee( + "Test", 1, makerFee, takerFee ) coEvery { diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt index 7c4d1f8f7..c1cefcb8b 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/FeeConfigRepository.kt @@ -1,6 +1,7 @@ package co.nilin.opex.accountant.ports.postgres.dao import co.nilin.opex.accountant.core.model.FeeConfig +import co.nilin.opex.accountant.core.model.UserFee import co.nilin.opex.accountant.ports.postgres.model.FeeConfigModel import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.reactive.ReactiveCrudRepository @@ -19,7 +20,7 @@ interface FeeConfigRepository : ReactiveCrudRepository { @Query( """ - SELECT * + SELECT name,display_order,maker_fee,taker_fee FROM fee_config WHERE ( @@ -39,6 +40,6 @@ interface FeeConfigRepository : ReactiveCrudRepository { LIMIT 1 """ ) - fun findMatchingConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): Mono + fun findMatchingConfig(assetVolume: BigDecimal, tradeVolume: BigDecimal): Mono } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt index 4318627c2..13a8dae4d 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FeeConfigServiceImpl.kt @@ -1,6 +1,7 @@ package co.nilin.opex.accountant.ports.postgres.impl import co.nilin.opex.accountant.core.model.FeeConfig +import co.nilin.opex.accountant.core.model.UserFee import co.nilin.opex.accountant.core.spi.FeeConfigService import co.nilin.opex.accountant.ports.postgres.dao.FeeConfigRepository import co.nilin.opex.accountant.ports.postgres.model.FeeConfigModel @@ -45,7 +46,7 @@ class FeeConfigServiceImpl( override suspend fun loadMatchingFeeConfig( assetVolume: BigDecimal, tradeVolume: BigDecimal - ): FeeConfig { + ): UserFee { return feeConfigRepository.findMatchingConfig(assetVolume, tradeVolume).awaitFirst() } diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt new file mode 100644 index 000000000..176b9757d --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserFee.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.api.core.inout + +import java.math.BigDecimal + +data class UserFee( + val name: String, + val displayOrder: Int, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + + ) + + diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt index 06cbc0587..6f1b2a64f 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/AccountantProxy.kt @@ -1,8 +1,8 @@ package co.nilin.opex.api.core.spi import co.nilin.opex.api.core.inout.FeeConfig -import co.nilin.opex.api.core.inout.PairFeeResponse import co.nilin.opex.api.core.inout.PairConfigResponse +import co.nilin.opex.api.core.inout.UserFee interface AccountantProxy { @@ -10,6 +10,5 @@ interface AccountantProxy { fun getFeeConfigs(): List - fun getFeeConfig(symbol: String): PairFeeResponse - + fun getUserFee(uuid: String): UserFee } \ No newline at end of file diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt index 4f7793157..e91de1b06 100644 --- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/MarketController.kt @@ -8,6 +8,8 @@ import co.nilin.opex.api.ports.opex.data.OrderBookResponse import co.nilin.opex.api.ports.opex.data.RecentTradeResponse import co.nilin.opex.common.OpexError import co.nilin.opex.common.utils.Interval +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.* import java.math.BigDecimal import java.time.ZoneId @@ -57,7 +59,7 @@ class MarketController( } @GetMapping("/fee") - fun getPairFees(): List { + fun getFeeConfigs(): List { return accountantProxy.getFeeConfigs() } diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt index be849eb15..756241e94 100644 --- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserDataController.kt @@ -1,17 +1,25 @@ package co.nilin.opex.api.ports.opex.controller import co.nilin.opex.api.core.inout.UserCurrencyVolume +import co.nilin.opex.api.core.inout.UserFee import co.nilin.opex.api.core.inout.UserTotalVolumeValue +import co.nilin.opex.api.core.spi.AccountantProxy import co.nilin.opex.api.core.spi.MarketUserDataProxy import co.nilin.opex.common.OpexError import co.nilin.opex.common.utils.Interval import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/opex/v1/user/data") -class UserDataController(private val proxy: MarketUserDataProxy) { +class UserDataController( + private val proxy: MarketUserDataProxy, + private val accountantProxy: AccountantProxy, +) { @GetMapping("/trade/volume") suspend fun getTradeVolumeByCurrency( @@ -34,6 +42,11 @@ class UserDataController(private val proxy: MarketUserDataProxy) { return proxy.getTotalTradeVolumeValue(uuid, interval) } + @GetMapping("/fee") + fun getUserFee(@CurrentSecurityContext securityContext: SecurityContext): UserFee { + return accountantProxy.getUserFee(securityContext.authentication.name) + } + private fun checkValidInterval(interval: Interval) { if (interval == Interval.Day || interval == Interval.Week || interval == Interval.Month || interval == Interval.Year) return diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt index 2f58e0771..d8b684241 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/AccountantProxyImpl.kt @@ -2,9 +2,10 @@ package co.nilin.opex.api.ports.proxy.impl import co.nilin.opex.api.core.inout.FeeConfig import co.nilin.opex.api.core.inout.PairConfigResponse -import co.nilin.opex.api.core.inout.PairFeeResponse +import co.nilin.opex.api.core.inout.UserFee import co.nilin.opex.api.core.spi.AccountantProxy import co.nilin.opex.api.ports.proxy.utils.noBody +import co.nilin.opex.common.OpexError import co.nilin.opex.common.utils.LoggerDelegate import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpMethod @@ -32,18 +33,18 @@ class AccountantProxyImpl(private val restTemplate: RestTemplate) : AccountantPr override fun getFeeConfigs(): List { logger.info("fetching fee configs") return restTemplate.exchange>( - "$baseUrl/config/fee", + "$baseUrl/fee/config", HttpMethod.GET, noBody() ).body?.toList() ?: emptyList() } - override fun getFeeConfig(symbol: String): PairFeeResponse { - logger.info("fetching fee configs for $symbol") - return restTemplate.exchange( - "$baseUrl/config/fee/$symbol", + override fun getUserFee(uuid: String): UserFee { + logger.info("fetching user fee") + return restTemplate.exchange( + "$baseUrl/fee/${uuid}", HttpMethod.GET, noBody() - ).body!! + ).body ?: throw OpexError.FeeConfigNotFound.exception() } } \ No newline at end of file