diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6b8a31ef6..145809528 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,9 +22,9 @@ jobs: java-version: ${{ matrix.java }} # cache: maven - name: Build - run: mvn -B -T 1C clean install -Potc + run: mvn -B clean install -Potc - name: Run Tests - run: mvn -B -T 1C -Dskip.unit.tests=false surefire:test + run: mvn -B -Dskip.unit.tests=false surefire:test - name: Build Docker images env: TAG: pr diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt index e5e4eaccf..986b8ba09 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderData.kt @@ -12,7 +12,8 @@ data class OrderData( val quantity: BigDecimal, val takerFee: BigDecimal, val makerFee: BigDecimal, - val status: OrderStatus, + val status: Int, + val appearance: Int, val createDate: LocalDateTime, val updateDate: LocalDateTime, -) +) \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt index aba4d3351..bac8c0752 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt @@ -6,16 +6,42 @@ enum class TimeInForce { FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution. } -enum class OrderStatus(val code: Int) { +enum class OrderStatus(val code: Int, val orderOfAppearance: Int) { - REQUESTED(0), - NEW(1), //The order has been accepted by the engine. - PARTIALLY_FILLED(4), //A part of the order has been filled. - FILLED(5), //The order has been completed. - CANCELED(2), //The order has been canceled by the user. - REJECTED(3), //The order was not accepted by the engine and not processed. - EXPIRED(6); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) + REQUESTED(0, 0), + NEW(1, 1), //The order has been accepted by the engine. + PARTIALLY_FILLED(4, 2), //A part of the order has been filled. + FILLED(5, 3), //The order has been completed. + CANCELED(2, 3), //The order has been canceled by the user. + REJECTED(3, 3), //The order was not accepted by the engine and not processed. + EXPIRED( + 6, + 3 + ); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) + fun comesBefore(status: OrderStatus?): Boolean { + if (status == null) + return false + return orderOfAppearance < status.orderOfAppearance + } + + fun comesAfter(status: OrderStatus?): Boolean { + if (status == null) + return false + return orderOfAppearance > status.orderOfAppearance + } + + fun isOpenOrder(): Boolean { + return this == NEW || this == PARTIALLY_FILLED + } + + companion object { + fun fromCode(code: Int?): OrderStatus? { + if (code == null) + return null + return values().find { it.code == code } + } + } fun isWorking(): Boolean { return listOf(NEW, PARTIALLY_FILLED).contains(this) } diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt new file mode 100644 index 000000000..efea1245f --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/SwapResponse.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.api.core.inout + +import java.math.BigDecimal +import java.time.LocalDateTime + +data class SwapResponse( + var reserveNumber: String, + var sourceSymbol: String, + var destSymbol: String, + var uuid: String, + var sourceAmount: BigDecimal, + var reservedDestAmount: BigDecimal, + var reserveDate: LocalDateTime? = LocalDateTime.now(), + var expDate: LocalDateTime? = null, + var status: ReservedStatus? = null, + val rate: BigDecimal? = null, +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt index 25c36abf6..814642140 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/UserTransactionRequest.kt @@ -1,11 +1,19 @@ package co.nilin.opex.api.core.inout data class UserTransactionRequest( + val userId: String? = null, val currency: String?, + val sourceSymbol: String?, + val destSymbol: String?, val category: UserTransactionCategory?, val startTime: Long? = null, val endTime: Long? = null, val limit: Int? = 10, val offset: Int? = 0, - val ascendingByTime: Boolean? = false, -) \ No newline at end of file + val ascendingByTime: Boolean = false, + val status: ReservedStatus? = ReservedStatus.Committed +) + +enum class ReservedStatus { + Created, Expired, Committed, +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt index bf6ac578f..7f83cb162 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/WalletProxy.kt @@ -123,5 +123,6 @@ interface WalletProxy { suspend fun getQuoteCurrencies(): List - suspend fun getSwapTransactionsCount(token: String, request: UserTransactionRequest):Long + suspend fun getSwapTransactions(token: String, request: UserTransactionRequest): List + suspend fun getSwapTransactionsCount(token: String, request: UserTransactionRequest): Long } \ 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/UserHistoryController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserHistoryController.kt index 93121fe68..0e2335fcf 100644 --- a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserHistoryController.kt +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserHistoryController.kt @@ -3,16 +3,13 @@ package co.nilin.opex.api.ports.opex.controller import co.nilin.opex.api.core.inout.* import co.nilin.opex.api.core.spi.MarketUserDataProxy import co.nilin.opex.api.core.spi.WalletProxy +import co.nilin.opex.api.ports.opex.data.OrderDataResponse import co.nilin.opex.api.ports.opex.util.jwtAuthentication +import co.nilin.opex.api.ports.opex.util.toResponse import co.nilin.opex.api.ports.opex.util.tokenValue import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -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("/opex/v1/user") @@ -31,7 +28,7 @@ class UserHistoryController( @RequestParam limit: Int?, @RequestParam offset: Int?, @CurrentSecurityContext securityContext: SecurityContext, - ): List { + ): List { return marketUserDataProxy.getOrderHistory( securityContext.authentication.name, symbol, @@ -41,7 +38,7 @@ class UserHistoryController( direction, limit ?: 10, offset ?: 0, - ) + ).map { it.toResponse() } } @GetMapping("/history/order/count") @@ -256,6 +253,15 @@ class UserHistoryController( limit, ) } + + @PostMapping("/history/swap") + suspend fun getSwapHistory( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestBody request: UserTransactionRequest + ): List { + return walletProxy.getSwapTransactions(securityContext.jwtAuthentication().tokenValue(), request) + } + @PostMapping("/history/swap/count") suspend fun getSwapHistoryCount( @CurrentSecurityContext securityContext: SecurityContext, diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderDataResponse.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderDataResponse.kt new file mode 100644 index 000000000..7745f91da --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/data/OrderDataResponse.kt @@ -0,0 +1,21 @@ +package co.nilin.opex.api.ports.opex.data + +import co.nilin.opex.api.core.inout.MatchingOrderType +import co.nilin.opex.api.core.inout.OrderDirection +import co.nilin.opex.api.core.inout.OrderStatus +import java.math.BigDecimal +import java.time.LocalDateTime +import java.util.* + +data class OrderDataResponse( + val symbol: String, + val orderType: MatchingOrderType, + val side: OrderDirection, + val price: BigDecimal, + val quantity: BigDecimal, + val takerFee: BigDecimal, + val makerFee: BigDecimal, + val status: OrderStatus, + val createDate: LocalDateTime, + val updateDate: LocalDateTime, +) \ No newline at end of file diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/ConvertorExtenstions.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/ConvertorExtenstions.kt new file mode 100644 index 000000000..0e398f2a3 --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/util/ConvertorExtenstions.kt @@ -0,0 +1,20 @@ +package co.nilin.opex.api.ports.opex.util + +import co.nilin.opex.api.core.inout.OrderData +import co.nilin.opex.api.core.inout.OrderStatus +import co.nilin.opex.api.ports.opex.data.OrderDataResponse + +fun OrderData.toResponse(): OrderDataResponse { + return OrderDataResponse( + symbol = this.symbol, + orderType = this.orderType, + side = this.side, + price = this.price, + quantity = this.quantity, + takerFee = this.takerFee, + makerFee = this.makerFee, + status = OrderStatus.fromCode(this.status) ?: OrderStatus.REJECTED, + createDate = this.createDate, + updateDate = this.updateDate, + ) +} \ No newline at end of file diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt index cc7fb4555..c934ad904 100644 --- a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/WalletProxyImpl.kt @@ -181,13 +181,17 @@ class WalletProxyImpl(private val webClient: WebClient) : WalletProxy { .body( Mono.just( UserTransactionRequest( + null, currency, + null, + null, category, startTime, endTime, limit, offset, - ascendingByTime + ascendingByTime == true, + null ) ) ) @@ -210,7 +214,20 @@ class WalletProxyImpl(private val webClient: WebClient) : WalletProxy { .uri("$baseUrl/v2/transaction/count") .accept(MediaType.APPLICATION_JSON) .header(HttpHeaders.AUTHORIZATION, "Bearer $token") - .body(Mono.just(UserTransactionRequest(currency, category, startTime, endTime))) + .body( + Mono.just( + UserTransactionRequest( + null, + currency, + null, + null, + category, + startTime, + endTime, + null + ) + ) + ) .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) .bodyToMono() @@ -409,6 +426,19 @@ class WalletProxyImpl(private val webClient: WebClient) : WalletProxy { } } + override suspend fun getSwapTransactions(token: String, request: UserTransactionRequest): List { + return webClient.post() + .uri("$baseUrl/v1/swap/history") + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .body(Mono.just(request)) + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToFlux() + .collectList() + .awaitFirstOrElse { emptyList() } + } + override suspend fun getSwapTransactionsCount( token: String, request: UserTransactionRequest diff --git a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt index ccdd5f812..fbbf28166 100644 --- a/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt +++ b/market/market-app/src/main/kotlin/co/nilin/opex/market/app/controller/UserDataController.kt @@ -87,8 +87,6 @@ class UserDataController(private val userQueryHandler: UserQueryHandler) { @RequestParam endTime: Long?, @RequestParam orderType: MatchingOrderType?, @RequestParam direction: OrderDirection?, - @RequestParam limit: Int?, - @RequestParam offset: Int?, @PathVariable uuid: String, ): Long { return userQueryHandler.getOrderHistoryCount( diff --git a/wallet/wallet-app/pom.xml b/wallet/wallet-app/pom.xml index c8ecddab5..db64553b0 100644 --- a/wallet/wallet-app/pom.xml +++ b/wallet/wallet-app/pom.xml @@ -66,6 +66,10 @@ io.projectreactor.kotlin reactor-kotlin-extensions + + org.springframework.boot + spring-boot-starter-data-redis + org.jetbrains.kotlin kotlin-reflect diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/CacheConfig.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/CacheConfig.kt new file mode 100644 index 000000000..170f1bab1 --- /dev/null +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/config/CacheConfig.kt @@ -0,0 +1,40 @@ +package co.nilin.opex.wallet.app.config + +import co.nilin.opex.wallet.ports.postgres.util.CacheHelper +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.cache.RedisCacheConfiguration +import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder +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.RedisSerializationContext +import org.springframework.data.redis.serializer.StringRedisSerializer +import java.time.Duration + +@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/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt index ab0657d59..fc69f34ae 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/WalletOwnerController.kt @@ -22,7 +22,8 @@ class WalletOwnerController( private val walletOwnerManager: WalletOwnerManager, private val walletManager: WalletManager, private val environment: Environment, - private val currentUserProvider: CurrentUserProvider + private val currentUserProvider: CurrentUserProvider, + private val balanceParser: BalanceParser, ) { @GetMapping("/{uuid}/wallets") @@ -45,7 +46,7 @@ class WalletOwnerController( throw OpexError.WalletOwnerNotFound.exception() } val wallets = walletManager.findWalletsByOwner(owner) - return BalanceParser.parse(wallets) + return balanceParser.parse(wallets) } @GetMapping("/{uuid}/wallets/{symbol}") @@ -62,7 +63,7 @@ class WalletOwnerController( suspend fun getWallet(@PathVariable uuid: String, @PathVariable symbol: String): WalletData { val owner = walletOwnerManager.findWalletOwner(uuid) ?: throw OpexError.WalletOwnerNotFound.exception() val wallets = walletManager.findWalletByOwnerAndSymbol(owner, symbol) - return BalanceParser.parseSingleCurrency(wallets) ?: throw OpexError.WalletNotFound.exception() + return balanceParser.parseSingleCurrency(wallets) ?: throw OpexError.WalletNotFound.exception() } @GetMapping("/{uuid}/limits") diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt index 06250723b..90db9f1de 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/TransferService.kt @@ -12,8 +12,8 @@ import co.nilin.opex.wallet.core.model.TransferCategory import co.nilin.opex.wallet.core.model.WalletType import co.nilin.opex.wallet.core.model.otc.Rate import co.nilin.opex.wallet.core.model.otc.ReservedTransfer +import co.nilin.opex.wallet.core.service.PrecisionService import co.nilin.opex.wallet.core.spi.* -import io.micrometer.core.instrument.MeterRegistry import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -30,8 +30,7 @@ class TransferService( private val walletOwnerManager: WalletOwnerManager, private val currencyGraph: GraphService, private val reservedTransferManager: ReservedTransferManager, - private val meterRegistry: MeterRegistry, - private val currencyService: CurrencyServiceV2, + private val precisionService: PrecisionService, ) { @@ -93,13 +92,16 @@ class TransferService( receiverUuid: String, receiverWalletType: WalletType, ): ReservedTransferResponse { + precisionService.validatePrecision(sourceAmount, sourceSymbol) val rate = currencyGraph.buildRoutes(sourceSymbol, destSymbol) .map { route -> Rate(route.getSourceSymbol(), route.getDestSymbol(), route.getRate()) } .firstOrNull() ?: throw OpexError.NOT_EXCHANGEABLE_CURRENCIES.exception() val finalAmount = sourceAmount.multiply(rate.rate) - if (sourceAmount == BigDecimal.ZERO || finalAmount == BigDecimal.ZERO) throw OpexError.InvalidAmount.exception() + val destAmount = precisionService.calculatePrecision(finalAmount, destSymbol) + validateMinimumAmount(sourceSymbol, sourceAmount, destAmount, destSymbol, rate.rate) + precisionService.validatePrecision(destAmount, destSymbol) checkIfSystemHasEnoughBalance(destSymbol, receiverWalletType, finalAmount) val reserveNumber = UUID.randomUUID().toString() @@ -266,4 +268,32 @@ class TransferService( throw OpexError.CurrentSystemAssetsAreNotEnough.exception() } } + + private suspend fun validateMinimumAmount( + sourceSymbol: String, + sourceAmount: BigDecimal, + destAmount: BigDecimal, + destSymbol: String, + rate: BigDecimal + ) { + fun minPrecisionAmount(precision: Int): BigDecimal = + BigDecimal.ONE.scaleByPowerOfTen(-precision) + + val sourcePrecision = precisionService.getPrecision(sourceSymbol).toInt() + val destPrecision = precisionService.getPrecision(destSymbol).toInt() + + val minSourceAmount = minPrecisionAmount(sourcePrecision) + val minDestAmount = minPrecisionAmount(destPrecision) + + val minimumSource = + maxOf(minSourceAmount, minDestAmount.divide(rate)).setScale(sourcePrecision, RoundingMode.DOWN) + val minimumDest = + maxOf(minDestAmount, minSourceAmount.multiply(rate)).setScale(destPrecision, RoundingMode.DOWN) + + if (sourceAmount < minimumSource || destAmount < minimumDest) { + throw OpexError.InvalidAmount.exception("amount is lower than minimum") + } + } + + } \ No newline at end of file diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/utils/BalanceParser.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/utils/BalanceParser.kt index d98b77528..6218b3703 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/utils/BalanceParser.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/utils/BalanceParser.kt @@ -3,9 +3,15 @@ package co.nilin.opex.wallet.app.utils import co.nilin.opex.wallet.app.dto.WalletData import co.nilin.opex.wallet.core.model.Wallet import co.nilin.opex.wallet.core.model.WalletType +import co.nilin.opex.wallet.core.service.PrecisionService +import co.nilin.opex.wallet.ports.postgres.impl.PrecisionServiceImpl +import org.springframework.stereotype.Component import java.math.BigDecimal -object BalanceParser { +@Component +class BalanceParser( + private val precisionService: PrecisionService +) { fun parse(list: List): List { val result = arrayListOf() @@ -13,9 +19,9 @@ object BalanceParser { for (w in list) { result.addOrGet(w.currency.symbol).apply { when (w.type) { - WalletType.MAIN -> balance = w.balance.amount - WalletType.EXCHANGE -> locked = w.balance.amount - WalletType.CASHOUT -> withdraw = w.balance.amount + WalletType.MAIN -> balance = precisionService.calculatePrecision(w.balance.amount, w.currency.symbol) + WalletType.EXCHANGE -> locked = precisionService.calculatePrecision(w.balance.amount, w.currency.symbol) + WalletType.CASHOUT -> withdraw = precisionService.calculatePrecision(w.balance.amount, w.currency.symbol) } } } @@ -32,9 +38,12 @@ object BalanceParser { throw IllegalStateException("Found multiple currencies while parsing for single") when (w.type) { - WalletType.MAIN -> result.balance = w.balance.amount - WalletType.EXCHANGE -> result.locked = w.balance.amount - WalletType.CASHOUT -> result.withdraw = w.balance.amount + WalletType.MAIN -> result.balance = precisionService.calculatePrecision(w.balance.amount, w.currency.symbol) + WalletType.EXCHANGE -> result.locked = + precisionService.calculatePrecision(w.balance.amount, w.currency.symbol) + + WalletType.CASHOUT -> result.withdraw = + precisionService.calculatePrecision(w.balance.amount, w.currency.symbol) } } return result diff --git a/wallet/wallet-app/src/main/resources/application.yml b/wallet/wallet-app/src/main/resources/application.yml index fd4347ed8..cdf93e5c1 100644 --- a/wallet/wallet-app/src/main/resources/application.yml +++ b/wallet/wallet-app/src/main/resources/application.yml @@ -30,6 +30,11 @@ spring: username: ${dbusername:opex} password: ${dbpassword:hiopex} initialization-mode: always + cache: + type: redis + redis: + host: redis-cache + port: 6379 cloud: bootstrap: enabled: true diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/CurrencyPrecision.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/CurrencyPrecision.kt new file mode 100644 index 000000000..693bd3622 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/CurrencyPrecision.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.wallet.core.inout + + +import java.math.BigDecimal + +data class CurrencyPrecision( + var symbol: String, + var precision: BigDecimal, +) diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/PrecisionService.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/PrecisionService.kt new file mode 100644 index 000000000..12131e48b --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/PrecisionService.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.wallet.core.service + +import java.math.BigDecimal + +interface PrecisionService { + fun calculatePrecision(amount: BigDecimal, symbol: String): BigDecimal + + suspend fun validatePrecision(amount: BigDecimal, symbol: String) + + suspend fun getPrecision(symbol: String): BigDecimal +} \ No newline at end of file diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt index a4bf55f10..b6738159f 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/WithdrawService.kt @@ -26,6 +26,7 @@ class WithdrawService( private val environment: Environment, private val meterRegistry: MeterRegistry, private val gatewayService: GatewayService, + private val precisionService: PrecisionService, @Qualifier("onChainGateway") private val bcGatewayProxy: GatewayPersister, @Value("\${app.system.uuid}") private val systemUuid: String, ) { @@ -33,7 +34,7 @@ class WithdrawService( @Transactional suspend fun requestWithdraw(withdrawCommand: WithdrawCommand): WithdrawActionResult { - + precisionService.validatePrecision(withdrawCommand.amount, withdrawCommand.currency) val currency = currencyService.fetchCurrency(FetchCurrency(symbol = withdrawCommand.currency)) ?: throw OpexError.CurrencyNotFound.exception() diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/CurrencyServiceManager.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/CurrencyServiceManager.kt index a42bd77ff..12557b73b 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/CurrencyServiceManager.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/CurrencyServiceManager.kt @@ -5,6 +5,7 @@ package co.nilin.opex.wallet.core.spi import co.nilin.opex.wallet.core.inout.CurrenciesCommand import co.nilin.opex.wallet.core.inout.CurrencyCommand import co.nilin.opex.wallet.core.inout.CurrencyData +import co.nilin.opex.wallet.core.inout.CurrencyPrecision import co.nilin.opex.wallet.core.model.FetchCurrency @@ -19,4 +20,5 @@ interface CurrencyServiceManager { // suspend fun prepareCurrencyToBeACryptoCurrency(request: String): CurrencyCommand? suspend fun deleteCurrency(request: FetchCurrency): Void? + suspend fun fetchAllCurrenciesPrecision(): List } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/pom.xml b/wallet/wallet-ports/wallet-persister-postgres/pom.xml index 3e7fcb0b5..c79397b8b 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/pom.xml +++ b/wallet/wallet-ports/wallet-persister-postgres/pom.xml @@ -29,6 +29,10 @@ r2dbc-postgresql runtime + + org.springframework.boot + spring-boot-starter-data-redis + org.postgresql postgresql diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepositoryV2.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepositoryV2.kt index 0e368ca6c..942dad67f 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepositoryV2.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRepositoryV2.kt @@ -1,5 +1,6 @@ package co.nilin.opex.wallet.ports.postgres.dao +import co.nilin.opex.wallet.core.inout.CurrencyPrecision import co.nilin.opex.wallet.ports.postgres.model.CurrencyModel import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.reactive.ReactiveCrudRepository @@ -47,4 +48,6 @@ interface CurrencyRepositoryV2 : ReactiveCrudRepository { @Query("select * from currency order by display_order ") fun fetchAll(): Flux + @Query("select symbol,precision from currency") + fun fetchAllCurrenciesPrecision(): Flux } diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceImplV2.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceImplV2.kt index 27ff62867..e01d2578f 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceImplV2.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceImplV2.kt @@ -7,10 +7,12 @@ import co.nilin.opex.common.OpexError import co.nilin.opex.wallet.core.inout.CurrenciesCommand import co.nilin.opex.wallet.core.inout.CurrencyCommand import co.nilin.opex.wallet.core.inout.CurrencyData +import co.nilin.opex.wallet.core.inout.CurrencyPrecision import co.nilin.opex.wallet.core.model.FetchCurrency import co.nilin.opex.wallet.core.spi.CurrencyServiceManager import co.nilin.opex.wallet.ports.postgres.dao.CurrencyRepositoryV2 import co.nilin.opex.wallet.ports.postgres.model.CurrencyModel +import co.nilin.opex.wallet.ports.postgres.util.RedisCacheHelper import co.nilin.opex.wallet.ports.postgres.util.toCommand import co.nilin.opex.wallet.ports.postgres.util.toCurrencyData import co.nilin.opex.wallet.ports.postgres.util.toModel @@ -23,7 +25,8 @@ import reactor.core.publisher.Mono import java.util.stream.Collectors @Service("newVersion") -class CurrencyServiceImplV2(val currencyRepository: CurrencyRepositoryV2) : CurrencyServiceManager { +class CurrencyServiceImplV2(val currencyRepository: CurrencyRepositoryV2, val redisCacheHelper: RedisCacheHelper) : + CurrencyServiceManager { private val logger = LoggerFactory.getLogger(CurrencyServiceImplV2::class.java) @@ -35,6 +38,7 @@ class CurrencyServiceImplV2(val currencyRepository: CurrencyRepositoryV2) : Curr return null } ?: run { return doPersist(request.toModel())?.toCommand() + .also { redisCacheHelper.put("${request.symbol}-precision", request.precision) } } } @@ -43,6 +47,7 @@ class CurrencyServiceImplV2(val currencyRepository: CurrencyRepositoryV2) : Curr return loadCurrency(FetchCurrency(symbol = request.symbol)) ?.awaitFirstOrNull()?.let { doSave(it.toCommand().updateTo(request).toModel())?.toCommand() + .also { redisCacheHelper.put("${request.symbol}-precision", request.precision) } } ?: throw OpexError.CurrencyNotFound.exception() } @@ -58,6 +63,7 @@ class CurrencyServiceImplV2(val currencyRepository: CurrencyRepositoryV2) : Curr override suspend fun deleteCurrency(request: FetchCurrency): Void? { return loadCurrency(request)?.awaitFirstOrNull()?.let { currencyRepository.deleteById(it.symbol!!)?.awaitFirstOrNull() + .also { redisCacheHelper.evict("${request.symbol}-precision") } } } @@ -79,6 +85,10 @@ class CurrencyServiceImplV2(val currencyRepository: CurrencyRepositoryV2) : Curr return loadCurrency(request)?.awaitFirstOrNull()?.toCommand() } + override suspend fun fetchAllCurrenciesPrecision(): List { + return currencyRepository.fetchAllCurrenciesPrecision().collectList().awaitFirstOrElse { emptyList() } + } + private suspend fun loadCurrency(request: FetchCurrency): Mono? { if (request.uuid == null && request.symbol == null) throw OpexError.BadRequest.exception() diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/PrecisionServiceImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/PrecisionServiceImpl.kt new file mode 100644 index 000000000..027e8e348 --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/PrecisionServiceImpl.kt @@ -0,0 +1,61 @@ +package co.nilin.opex.wallet.ports.postgres.impl + +import co.nilin.opex.common.OpexError +import co.nilin.opex.wallet.core.model.FetchCurrency +import co.nilin.opex.wallet.core.service.PrecisionService +import co.nilin.opex.wallet.core.spi.CurrencyServiceManager +import co.nilin.opex.wallet.ports.postgres.util.RedisCacheHelper +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.math.RoundingMode + +@Component +class PrecisionServiceImpl( + private val redisCacheHelper: RedisCacheHelper, private val currencyServiceManager: CurrencyServiceManager +) : PrecisionService { + + //TODO optimize this + override fun calculatePrecision(amount: BigDecimal, symbol: String): BigDecimal { + val precision = redisCacheHelper.get("$symbol-precision")?.toInt() ?: return amount + + val scaledAmount = amount.setScale(precision, RoundingMode.DOWN) + if (scaledAmount != BigDecimal.ZERO.setScale(precision)) { + return scaledAmount + } + val decimalPart = amount.stripTrailingZeros().toPlainString().substringAfter('.', "") + + val zeroPrefixCount = decimalPart.indexOfFirst { it != '0' }.takeIf { it >= 0 } ?: 0 + val rest = decimalPart.drop(zeroPrefixCount) + + var nonZeroCount = 0 + var digitsToKeep = 0 + for (c in rest) { + digitsToKeep++ + if (c != '0') nonZeroCount++ + if (nonZeroCount == 2) break + } + val totalScale = zeroPrefixCount + digitsToKeep + + return amount.setScale(totalScale, RoundingMode.DOWN) + } + + + override suspend fun validatePrecision(amount: BigDecimal, symbol: String) { + val precision = redisCacheHelper.get("$symbol-precision") ?: (currencyServiceManager.fetchCurrency( + FetchCurrency(symbol = symbol) + )?.precision ?: throw OpexError.CurrencyNotFound.exception()) + + val actualScale = amount.stripTrailingZeros().scale() + + if (actualScale > precision.toInt()) { + throw OpexError.InvalidAmount.exception("Amount $amount exceeds allowed precision for $symbol (max $precision decimal places)") + } + } + + override suspend fun getPrecision(symbol: String): BigDecimal { + return redisCacheHelper.get("$symbol-precision") ?: (currencyServiceManager.fetchCurrency( + FetchCurrency(symbol = symbol) + )?.precision ?: throw OpexError.CurrencyNotFound.exception()) + } + +} \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/service/CurrencyCacheInitializer.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/service/CurrencyCacheInitializer.kt new file mode 100644 index 000000000..195d3bbdf --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/service/CurrencyCacheInitializer.kt @@ -0,0 +1,52 @@ +package co.nilin.opex.wallet.ports.postgres.service + +import co.nilin.opex.wallet.core.spi.CurrencyServiceManager +import co.nilin.opex.wallet.ports.postgres.util.RedisCacheHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import javax.annotation.PostConstruct + +@Service +class CurrencyCacheInitializer( + private val redisCacheHelper: RedisCacheHelper, private val currencyServiceManager: CurrencyServiceManager +) { + private val logger = LoggerFactory.getLogger(CurrencyCacheInitializer::class.java) + val scope = CoroutineScope(Dispatchers.IO) + + @PostConstruct + fun initialize() { + logger.info( + """ +================================================================================================ + Initialize Currency Cache +================================================================================================ + """ + ) + scope.launch { + try { + currencyServiceManager.fetchAllCurrenciesPrecision().forEach { currency -> + redisCacheHelper.put( + "${currency.symbol}-precision", currency.precision + ) + logger.info("${currency.symbol} : ${currency.precision}") + } + logger.info( + """ +================================================================================================ + Completed Successfully +================================================================================================ + """ + ) + + } catch (e: Exception) { + logger.error("Error initializing Currency Cache: ${e.message}") + throw e + } + + + } + } +} \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/CacheHelper.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/CacheHelper.kt new file mode 100644 index 000000000..49f33fc5d --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/CacheHelper.kt @@ -0,0 +1,91 @@ +package co.nilin.opex.wallet.ports.postgres.util + +import co.nilin.opex.common.utils.DynamicInterval +import co.nilin.opex.common.utils.LoggerDelegate +import org.springframework.cache.CacheManager + +class CacheHelper(private val manager: CacheManager, private val cacheName: String) { + + private val cache = manager.getCache(cacheName) ?: throw IllegalStateException("Cache $cacheName not found") + private val logger by LoggerDelegate() + + fun put(key: Any, value: Any?) { + logger.info("Putting cache with key $key") + cache.put(key, CacheValueWrapper(value)) + } + + fun putIfAbsent(key: Any, value: Any?) { + cache.putIfAbsent(key, CacheValueWrapper(value)) + } + + fun evict(key: Any) { + cache.evict(key) + } + + fun get(key: Any): Any? { + val value = cache.get(key) ?: return null + val item = value.get() ?: return null + + if (item is CacheValueWrapper) { + if (item.checkTimeToEvict()) { + cache.evict(key) + logger.info("Cache $key evicted") + } + return item.value + } + + logger.info("Read from cache with $key") + return item + } + + @Suppress("UNCHECKED_CAST") + suspend fun getOrElse(key: Any, put: Boolean = true, action: suspend () -> T): T { + val value = get(key) + if (value != null) + return value as T + + val item = action() + if (put) put(key, item) + return item + } + + fun putTimeBased(key: Any, value: Any?, evictionTime: Long, ignoreNullOrEmpty: Boolean = true) { + logger.info("Putting time based cache with key $key") + // Do not put if item is a Collection and is empty + if (value == null || (value is Collection<*> && ignoreNullOrEmpty && value.isEmpty())) return + val cacheValueWrapper = CacheValueWrapper(value, evictionTime) + cache.putIfAbsent(key, cacheValueWrapper) + } + + fun putTimeBased(key: Any, value: Any, evictIn: DynamicInterval, ignoreNullOrEmpty: Boolean = true) { + putTimeBased(key, value, evictIn.timeInFuture(), ignoreNullOrEmpty) + } + + @Suppress("UNCHECKED_CAST") + suspend fun getTimeBasedOrElse( + key: Any, + evictionTime: Long, + put: Boolean = true, + ignoreNullOrEmpty: Boolean = true, + action: suspend () -> T + ): T { + val value = get(key) + if (value != null) + return value as T + + val item = action() + if (put) putTimeBased(key, item, evictionTime, ignoreNullOrEmpty) + return item + } + + suspend fun getTimeBasedOrElse( + key: Any, + evictIn: DynamicInterval, + put: Boolean = true, + ignoreNullOrEmpty: Boolean = true, + action: suspend () -> T + ): T { + return getTimeBasedOrElse(key, evictIn.timeInFuture(), put, ignoreNullOrEmpty, action) + } + +} \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/CacheValueWrapper.java b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/CacheValueWrapper.java new file mode 100644 index 000000000..1895c5b88 --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/CacheValueWrapper.java @@ -0,0 +1,57 @@ +package co.nilin.opex.wallet.ports.postgres.util; + +import java.util.Date; + +public class CacheValueWrapper { + + private Object value; + private long evictionTime; + private boolean isTimeBased; + + public CacheValueWrapper() { + + } + + public CacheValueWrapper(Object value) { + this.value = value; + this.evictionTime = -1; + this.isTimeBased = false; + } + + public CacheValueWrapper(Object value, long evictionTime) { + this.value = value; + if (evictionTime < 0) + throw new IllegalStateException("Eviction time must be greater than zero"); + + this.evictionTime = evictionTime; + this.isTimeBased = true; + } + + public boolean checkTimeToEvict() { + return isTimeBased && evictionTime < new Date().getTime(); + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public long getEvictionTime() { + return evictionTime; + } + + public void setEvictionTime(long evictionTime) { + this.evictionTime = evictionTime; + } + + public boolean isTimeBased() { + return isTimeBased; + } + + public void setTimeBased(boolean timeBased) { + isTimeBased = timeBased; + } +} diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/RedisCacheHelper.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/RedisCacheHelper.kt new file mode 100644 index 000000000..96eb58230 --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/util/RedisCacheHelper.kt @@ -0,0 +1,104 @@ +package co.nilin.opex.wallet.ports.postgres.util + +import co.nilin.opex.common.utils.DynamicInterval +import co.nilin.opex.common.utils.LoggerDelegate +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component + +@Component +class RedisCacheHelper(private val redisTemplate: RedisTemplate) { + + private val logger by LoggerDelegate() + + private val valueOps = redisTemplate.opsForValue() + private val listOps = redisTemplate.opsForList() + + fun put(key: String, value: Any?, expireAt: DynamicInterval? = null) { + value ?: return + try { + valueOps.set(key, value) + expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) } + } catch (e: Exception) { + logger.warn("Unable to put cache with key '$key'") + } + } + + fun putList(key: String, values: List, expireAt: DynamicInterval? = null) { + try { + values.forEach { listOps.rightPush(key, it) } + expireAt?.let { redisTemplate.expireAt(key, it.dateInFuture()) } + } catch (e: Exception) { + logger.warn("Unable to put list cache with key '$key'") + } + } + + fun putListItem(key: String, value: Any, rightPush: Boolean = true) { + try { + if (rightPush) + listOps.rightPush(key, value) + else + listOps.leftPush(key, value) + } catch (e: Exception) { + logger.warn("Unable to put list item cache with key '$key'") + } + } + + @Suppress("UNCHECKED_CAST") + fun get(key: String): T? { + return try { + valueOps.get(key) as T + } catch (e: Exception) { + logger.warn("Unable to get cache value with key '$key'") + null + } + } + + @Suppress("UNCHECKED_CAST") + fun getList(key: String): Collection? { + return try { + listOps.range(key, 0, -1) as Collection? + } catch (e: Exception) { + logger.warn("Unable to get cache list with key '$key'") + null + } + } + + suspend fun getOrElse(key: String, expireAt: DynamicInterval? = null, action: suspend () -> T): T { + val cacheValue = get(key) + return if (cacheValue != null) + cacheValue + else { + val value = action() + if (value != null) { + put(key, value) + expireAt?.let { setExpiration(key, it) } + } + return value + } + } + + fun evict(key: String) { + try { + redisTemplate.delete(key) + } catch (e: Exception) { + logger.warn("Unable to evict cache with key '$key'") + } + } + + fun setExpiration(key: String, interval: DynamicInterval) { + try { + redisTemplate.expireAt(key, interval.dateInFuture()) + } catch (e: Exception) { + logger.warn("Unable to set expiration date for cache with key '$key'") + } + } + + fun hasKey(key: String): Boolean { + return try { + redisTemplate.hasKey(key) + } catch (e: Exception) { + logger.warn("Unable fetch info of cache with key '$key'") + false + } + } +} \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt index b50dc7fa4..d33d6581d 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt @@ -3,6 +3,7 @@ package co.nilin.opex.wallet.ports.postgres.impl import co.nilin.opex.wallet.core.model.FetchCurrency import co.nilin.opex.wallet.ports.postgres.dao.CurrencyRepositoryV2 import co.nilin.opex.wallet.ports.postgres.impl.sample.VALID +import co.nilin.opex.wallet.ports.postgres.util.RedisCacheHelper import co.nilin.opex.wallet.ports.postgres.util.toModel import io.mockk.every import io.mockk.mockk @@ -13,7 +14,8 @@ import reactor.core.publisher.Mono private class CurrencyServiceTest { private val currencyRepository: CurrencyRepositoryV2 = mockk() - private val currencyService: CurrencyServiceImplV2 = CurrencyServiceImplV2(currencyRepository) + private val redisCacheHelper: RedisCacheHelper = mockk() + private val currencyService: CurrencyServiceImplV2 = CurrencyServiceImplV2(currencyRepository,redisCacheHelper) @Test fun givenCurrency_whenGetCurrency_thenReturnCurrency(): Unit = runBlocking {