diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt index e65c26d63..a02319314 100644 --- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/interceptor/APIKeyFilterImpl.kt @@ -28,6 +28,7 @@ class APIKeyFilterImpl( val signature = request.headers["X-API-SIGNATURE"]?.firstOrNull() val tsHeader = request.headers["X-API-TIMESTAMP"]?.firstOrNull() val uri = request.uri + val path = "/api" + uri.rawPath // HMAC path when signature present if (!apiKeyId.isNullOrBlank() && !signature.isNullOrBlank() && !tsHeader.isNullOrBlank()) { @@ -43,8 +44,8 @@ class APIKeyFilterImpl( logger.warn("API key {} request from disallowed IP {}", apiKeyId, sourceIp) null } - if (!entry.allowedEndpoints.isNullOrEmpty() && ( !entry.allowedEndpoints!!.contains(uri.rawPath))) { - logger.warn("API key {} request to unauthorized resource {}", apiKeyId, uri.rawPath) + if (!entry.allowedEndpoints.isNullOrEmpty() && ( !entry.allowedEndpoints!!.contains(path))) { + logger.warn("API key {} request to unauthorized resource {}", apiKeyId, path) null } else { val ts = tsHeader.toLongOrNull() @@ -58,7 +59,7 @@ class APIKeyFilterImpl( signature, HmacVerifier.VerificationInput( method = request.method.name(), - path = uri.rawPath, + path = path, query = uri.rawQuery, timestampMillis = ts, bodySha256 = bodyHash diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt new file mode 100644 index 000000000..5799b1044 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/analytics/ActivityTotals.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.api.core.inout.analytics + +import java.math.BigDecimal + +/** + * Totals for a single day of user activity (mock values for now). + */ +data class ActivityTotals( + val totalBalance: BigDecimal, + val totalWithdraw: BigDecimal, + val totalDeposit: BigDecimal, + val totalTrade: BigDecimal, + val totalSwap: BigDecimal +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyExchangeRatesResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyExchangeRatesResponse.kt new file mode 100644 index 000000000..5dfe7fb8e --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyExchangeRatesResponse.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.api.core.inout.otc + +import java.math.BigDecimal + +data class CurrencyExchangeRate( + val sourceSymbol: String, + val destSymbol: String, + val rate: BigDecimal +) + +data class CurrencyExchangeRatesResponse(val rates: List) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPair.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPair.kt new file mode 100644 index 000000000..bb2ec06bc --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPair.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.api.core.inout.otc + +import co.nilin.opex.common.OpexError + +data class CurrencyPair( + val sourceSymbol: String, + val destSymbol: String +) { + fun validate() { + if (sourceSymbol == destSymbol) + throw OpexError.SourceIsEqualDest.exception() + } +} diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPrice.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPrice.kt new file mode 100644 index 000000000..ac6536b5e --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/CurrencyPrice.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.api.core.inout.otc + +import java.math.BigDecimal + +data class CurrencyPrice(var currency: String, val buyPrice: BigDecimal? = null, var sellPrice: BigDecimal? = null) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/ForbiddenPair.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/ForbiddenPair.kt new file mode 100644 index 000000000..9dbbbf16d --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/ForbiddenPair.kt @@ -0,0 +1,10 @@ +package co.nilin.opex.api.core.inout.otc + +data class ForbiddenPair( + val sourceSymbol: String, + val destinationSymbol: String +) + +data class ForbiddenPairs( + var forbiddenPairs: List? +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Rate.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Rate.kt new file mode 100644 index 000000000..f1b9f3646 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Rate.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.api.core.inout.otc + +import java.math.BigDecimal + +data class Rate( + val sourceSymbol: String, + val destSymbol: String, + val rate: BigDecimal +) + +data class Rates( + var rates: List? +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/SetCurrencyExchangeRateRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/SetCurrencyExchangeRateRequest.kt new file mode 100644 index 000000000..ed2ee64c5 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/SetCurrencyExchangeRateRequest.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.api.core.inout.otc + +import co.nilin.opex.common.OpexError +import java.math.BigDecimal + +class SetCurrencyExchangeRateRequest( + val sourceSymbol: String, + val destSymbol: String, + val rate: BigDecimal, + var ignoreIfExist: Boolean? = false +) { + fun validate() { + if (rate <= BigDecimal.ZERO) + throw OpexError.InvalidRate.exception() + else if (sourceSymbol == destSymbol) + throw OpexError.SourceIsEqualDest.exception() + } +} diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Symbols.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Symbols.kt new file mode 100644 index 000000000..2ae155ad7 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/otc/Symbols.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.api.core.inout.otc + +data class Symbols(var symbols: List?) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateProxy.kt new file mode 100644 index 000000000..1be97ce42 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/RateProxy.kt @@ -0,0 +1,33 @@ +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.inout.otc.* + +interface RateProxy { + // Rates (writes require admin token) + suspend fun createRate(token: String, request: SetCurrencyExchangeRateRequest) + suspend fun updateRate(token: String, request: SetCurrencyExchangeRateRequest): Rates + suspend fun deleteRate(token: String, sourceSymbol: String, destSymbol: String): Rates + + // Rates (reads are public) + suspend fun fetchRates(): Rates + suspend fun fetchRate(sourceSymbol: String, destSymbol: String): Rate? + + // Forbidden pairs + suspend fun addForbiddenPair(token: String, request: CurrencyPair) + suspend fun deleteForbiddenPair(token: String, sourceSymbol: String, destSymbol: String): ForbiddenPairs + + // Forbidden pairs (read is public) + suspend fun fetchForbiddenPairs(): ForbiddenPairs + + // Transitive symbols + suspend fun addTransitiveSymbols(token: String, symbols: Symbols) + suspend fun deleteTransitiveSymbol(token: String, symbol: String): Symbols + suspend fun deleteTransitiveSymbols(token: String, symbols: Symbols): Symbols + + // Transitive symbols (read is public) + suspend fun fetchTransitiveSymbols(): Symbols + + // Routes and prices (reads are public) + suspend fun fetchRoutes(sourceSymbol: String? = null, destSymbol: String? = null): CurrencyExchangeRatesResponse + suspend fun getPrice(unit: String): List +} diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt index 99e9974f7..972a90826 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/config/SecurityConfig.kt @@ -50,7 +50,7 @@ class SecurityConfig( // Opex endpoints .pathMatchers("/opex/v1/admin/transactions/**").hasAnyAuthority("ROLE_monitoring", "ROLE_admin") .pathMatchers("/opex/v1/admin/**").hasAuthority("ROLE_admin") - .pathMatchers("/opex/v1/deposit/**").hasAuthority("DEPOSIT_deposit:write") + .pathMatchers("/opex/v1/deposit/**").hasAuthority("PERM_deposit:write") .pathMatchers(HttpMethod.POST, "/opex/v1/order").hasAuthority("PERM_order:write") .pathMatchers(HttpMethod.PUT, "/opex/v1/order").hasAuthority("PERM_order:write") .pathMatchers(HttpMethod.POST, "/opex/v1/withdraw").hasAuthority("PERM_withdraw:write") @@ -58,8 +58,11 @@ class SecurityConfig( .pathMatchers("/opex/v1/voucher").hasAuthority("PERM_voucher:submit") .pathMatchers("/opex/v1/market/**").permitAll() .pathMatchers(HttpMethod.GET, "/opex/v1/market/chain").permitAll() - .pathMatchers(HttpMethod.POST,"/v1/api-key").authenticated() + .pathMatchers(HttpMethod.POST, "/v1/api-key").authenticated() .pathMatchers("/v1/api-key").hasAuthority("ROLE_admin") + .pathMatchers(HttpMethod.PUT, "/opex/v1/otc/rate").hasAnyAuthority("ROLE_admin", "ROLE_rate_bot") + .pathMatchers(HttpMethod.GET, "/opex/v1/otc/**").permitAll() + .pathMatchers("/opex/v1/otc/**").hasAuthority("ROLE_admin") .anyExchange().authenticated() } .addFilterBefore(apiKeyFilter as WebFilter, SecurityWebFiltersOrder.AUTHENTICATION) diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/CurrencyRatesController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/CurrencyRatesController.kt new file mode 100644 index 000000000..717bfdd61 --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/CurrencyRatesController.kt @@ -0,0 +1,127 @@ +package co.nilin.opex.api.ports.opex.controller + +import co.nilin.opex.api.core.inout.otc.* +import co.nilin.opex.api.core.spi.RateProxy +import co.nilin.opex.api.ports.opex.util.jwtAuthentication +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.* + +@RestController +@RequestMapping( "/opex/v1/otc") +class CurrencyRatesController( + private val rateProxy: RateProxy +) { + + // Rates + @PostMapping("/rate") + suspend fun createRate( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestBody request: SetCurrencyExchangeRateRequest + ) { + request.validate() + rateProxy.createRate(securityContext.jwtAuthentication().tokenValue(), request) + } + + @PutMapping("/rate") + suspend fun updateRate( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestBody request: SetCurrencyExchangeRateRequest + ): Rates { + request.validate() + return rateProxy.updateRate(securityContext.jwtAuthentication().tokenValue(), request) + } + + @DeleteMapping("/rate/{sourceSymbol}/{destSymbol}") + suspend fun deleteRate( + @CurrentSecurityContext securityContext: SecurityContext, + @PathVariable sourceSymbol: String, + @PathVariable destSymbol: String + ): Rates { + return rateProxy.deleteRate(securityContext.jwtAuthentication().tokenValue(), sourceSymbol, destSymbol) + } + + @GetMapping("/rate") + suspend fun fetchRates(): Rates { + return rateProxy.fetchRates() + } + + @GetMapping("/rate/{sourceSymbol}/{destSymbol}") + suspend fun fetchRate( + @PathVariable sourceSymbol: String, + @PathVariable destSymbol: String + ): Rate? { + return rateProxy.fetchRate(sourceSymbol, destSymbol) + } + + // Forbidden pairs + @PostMapping("/forbidden-pairs") + suspend fun addForbiddenPair( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestBody request: CurrencyPair + ) { + request.validate() + rateProxy.addForbiddenPair(securityContext.jwtAuthentication().tokenValue(), request) + } + + @DeleteMapping("/forbidden-pairs/{sourceSymbol}/{destSymbol}") + suspend fun deleteForbiddenPair( + @CurrentSecurityContext securityContext: SecurityContext, + @PathVariable sourceSymbol: String, + @PathVariable destSymbol: String + ): ForbiddenPairs { + return rateProxy.deleteForbiddenPair(securityContext.jwtAuthentication().tokenValue(), sourceSymbol, destSymbol) + } + + @GetMapping("/forbidden-pairs") + suspend fun fetchForbiddenPairs(): ForbiddenPairs { + return rateProxy.fetchForbiddenPairs() + } + + // Transitive symbols + @PostMapping("/transitive-symbols") + suspend fun addTransitiveSymbols( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestBody symbols: Symbols + ) { + rateProxy.addTransitiveSymbols(securityContext.jwtAuthentication().tokenValue(), symbols) + } + + @DeleteMapping("/transitive-symbols/{symbol}") + suspend fun deleteTransitiveSymbols( + @CurrentSecurityContext securityContext: SecurityContext, + @PathVariable symbol: String + ): Symbols { + return rateProxy.deleteTransitiveSymbol(securityContext.jwtAuthentication().tokenValue(), symbol) + } + + @DeleteMapping("/transitive-symbols") + suspend fun deleteTransitiveSymbols( + @CurrentSecurityContext securityContext: SecurityContext, + @RequestBody symbols: Symbols + ): Symbols { + return rateProxy.deleteTransitiveSymbols(securityContext.jwtAuthentication().tokenValue(), symbols) + } + + @GetMapping("/transitive-symbols") + suspend fun fetchTransitiveSymbols(): Symbols { + return rateProxy.fetchTransitiveSymbols() + } + + // Routes and prices + @RequestMapping("/route", method = [RequestMethod.POST, RequestMethod.GET]) + suspend fun fetchRoutes( + @RequestParam("sourceSymbol") sourceSymbol: String? = null, + @RequestParam("destSymbol") destSymbol: String? = null + ): CurrencyExchangeRatesResponse { + return rateProxy.fetchRoutes(sourceSymbol, destSymbol) + } + + @GetMapping("/currency/price") + suspend fun getPrice( + @RequestParam("unit") unit: String + ): List { + return rateProxy.getPrice(unit) + } +} diff --git a/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserAnalyticsController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserAnalyticsController.kt new file mode 100644 index 000000000..6c86f7eaf --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/UserAnalyticsController.kt @@ -0,0 +1,86 @@ +package co.nilin.opex.api.ports.opex.controller + +import co.nilin.opex.api.core.inout.analytics.ActivityTotals +import co.nilin.opex.api.ports.opex.util.jwtAuthentication +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.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.* +import kotlin.random.Random + +@RestController +@RequestMapping("/opex/v1/analytics") +class UserAnalyticsController { + + @GetMapping("/user-activity") + suspend fun userActivity(@CurrentSecurityContext securityContext: SecurityContext): Map { + val jwt = securityContext.jwtAuthentication().token + val uuid = jwt.subject ?: "unknown" + + val zone = ZoneId.systemDefault() + val todayStart = LocalDate.now(zone).atStartOfDay(zone).toInstant() + + val days = 30 + val result = LinkedHashMap(days) + + // Initial balance seeded by user hash + var runningBalance = BigDecimal.valueOf(100 + (uuid.hashCode() and Int.MAX_VALUE) % 900L) + + for (i in (days - 1) downTo 0) { + val dayInstant = todayStart.minusSeconds(86400L * i) + val dayKey = dayInstant.toEpochMilli().toString() + + // deterministic seed per user+day + val seed = deterministicSeed(uuid.hashCode(), dayInstant.toEpochMilli()) + val rnd = Random(seed) + + val base = BigDecimal.valueOf((50 + rnd.nextInt(0, 950)).toLong()) // 50..999 + + val deposit = scaleMoney(base.multiply(BigDecimal.valueOf(rnd.nextDouble(0.0, 5.0)))) + val withdraw = scaleMoney(deposit.multiply(BigDecimal.valueOf(rnd.nextDouble(0.0, 0.9)))) + + val trade = scaleMoney(base.multiply(BigDecimal.valueOf(rnd.nextDouble(0.5, 8.0)))) + val swap = scaleMoney(base.multiply(BigDecimal.valueOf(rnd.nextDouble(0.0, 3.0)))) + + val pnlDrift = scaleMoney(trade.multiply(BigDecimal.valueOf(rnd.nextDouble(-0.01, 0.01)))) + + runningBalance = (runningBalance + deposit - withdraw + pnlDrift).coerceAtLeast(BigDecimal.ZERO) + + result[dayKey] = ActivityTotals( + totalBalance = runningBalance.min(MAX_BALANCE), + totalWithdraw = withdraw, + totalDeposit = deposit, + totalTrade = trade.min(MAX_TRADE), + totalSwap = swap.min(MAX_SWAP) + ) + } + + return result + } + + private fun scaleMoney(v: BigDecimal) = v.setScale(2, RoundingMode.HALF_UP) + + private val MAX_BALANCE = BigDecimal("10000000") + private val MAX_TRADE = BigDecimal("250000") + private val MAX_SWAP = BigDecimal("100000") + + /** + * Simple deterministic seed generator for a user and day. + * Combines user hash and day epoch millis into a reproducible seed. + */ + private fun deterministicSeed(userHash: Int, dayEpochMillis: Long): Long { + var seed = userHash.toLong() * 31 + dayEpochMillis + // simple bit mixing + seed = seed xor (seed shr 33) + seed *= 0xBF58476D1CE4E5BL + seed = seed xor (seed shr 33) + seed *= 0xc4ceb9fe1a85ec5L + seed = seed xor (seed shr 33) + return seed + } + +} diff --git a/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/RateProxyImpl.kt b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/RateProxyImpl.kt new file mode 100644 index 000000000..0258151b9 --- /dev/null +++ b/api/api-ports/api-proxy-rest/src/main/kotlin/co/nilin/opex/api/ports/proxy/impl/RateProxyImpl.kt @@ -0,0 +1,196 @@ +package co.nilin.opex.api.ports.proxy.impl + +import co.nilin.opex.api.core.inout.otc.* +import co.nilin.opex.api.core.spi.RateProxy +import co.nilin.opex.api.ports.proxy.config.ProxyDispatchers +import co.nilin.opex.common.utils.LoggerDelegate +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.withContext +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.body +import org.springframework.web.reactive.function.client.bodyToFlux +import org.springframework.web.reactive.function.client.bodyToMono +import reactor.core.publisher.Mono + +@Component +class RateProxyImpl(@Qualifier("generalWebClient") private val webClient: WebClient) : RateProxy { + + private val logger by LoggerDelegate() + + @Value("\${app.wallet.url}") + private lateinit var baseUrl: String + + // Rates + override suspend fun createRate(token: String, request: SetCurrencyExchangeRateRequest) { + withContext(ProxyDispatchers.wallet) { + webClient.post() + .uri("$baseUrl/otc/rate") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(request)) + .retrieve() + .toBodilessEntity() + .awaitFirstOrElse { null } + } + } + + override suspend fun updateRate(token: String, request: SetCurrencyExchangeRateRequest): Rates { + return withContext(ProxyDispatchers.wallet) { + webClient.put() + .uri("$baseUrl/otc/rate") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(request)) + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun deleteRate(token: String, sourceSymbol: String, destSymbol: String): Rates { + return withContext(ProxyDispatchers.wallet) { + webClient.delete() + .uri("$baseUrl/otc/rate/$sourceSymbol/$destSymbol") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun fetchRates(): Rates { + return withContext(ProxyDispatchers.wallet) { + webClient.get() + .uri("$baseUrl/otc/rate") + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun fetchRate(sourceSymbol: String, destSymbol: String): Rate? { + return withContext(ProxyDispatchers.wallet) { + webClient.get() + .uri("$baseUrl/otc/rate/$sourceSymbol/$destSymbol") + .retrieve() + .bodyToMono() + .awaitFirstOrNull() + } + } + + // Forbidden pairs + override suspend fun addForbiddenPair(token: String, request: CurrencyPair) { + withContext(ProxyDispatchers.wallet) { + webClient.post() + .uri("$baseUrl/otc/forbidden-pairs") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(request)) + .retrieve() + .toBodilessEntity() + .awaitFirstOrElse { null } + } + } + + override suspend fun deleteForbiddenPair(token: String, sourceSymbol: String, destSymbol: String): ForbiddenPairs { + return withContext(ProxyDispatchers.wallet) { + webClient.delete() + .uri("$baseUrl/otc/forbidden-pairs/$sourceSymbol/$destSymbol") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun fetchForbiddenPairs(): ForbiddenPairs { + return withContext(ProxyDispatchers.wallet) { + webClient.get() + .uri("$baseUrl/otc/forbidden-pairs") + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + // Transitive symbols + override suspend fun addTransitiveSymbols(token: String, symbols: Symbols) { + withContext(ProxyDispatchers.wallet) { + webClient.post() + .uri("$baseUrl/otc/transitive-symbols") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(symbols)) + .retrieve() + .toBodilessEntity() + .awaitFirstOrElse { null } + } + } + + override suspend fun deleteTransitiveSymbol(token: String, symbol: String): Symbols { + return withContext(ProxyDispatchers.wallet) { + webClient.delete() + .uri("$baseUrl/otc/transitive-symbols/$symbol") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun deleteTransitiveSymbols(token: String, symbols: Symbols): Symbols { + return withContext(ProxyDispatchers.wallet) { + webClient.method(HttpMethod.DELETE) + .uri("$baseUrl/otc/transitive-symbols") + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(symbols)) + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun fetchTransitiveSymbols(): Symbols { + return withContext(ProxyDispatchers.wallet) { + webClient.get() + .uri("$baseUrl/otc/transitive-symbols") + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + // Routes and prices + override suspend fun fetchRoutes(sourceSymbol: String?, destSymbol: String?): CurrencyExchangeRatesResponse { + var uri = "$baseUrl/otc/route" + if (sourceSymbol != null) uri = "$uri?sourceSymbol=$sourceSymbol" + if (destSymbol != null) uri = "$uri?destSymbol=$destSymbol" + return withContext(ProxyDispatchers.wallet) { + webClient.get() + .uri(uri) + .retrieve() + .bodyToMono() + .awaitSingle() + } + } + + override suspend fun getPrice(unit: String): List { + return withContext(ProxyDispatchers.wallet) { + webClient.get() + .uri("$baseUrl/otc/currency/price?unit=$unit") + .retrieve() + .bodyToFlux() + .collectList() + .awaitSingle() + } + } +} 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 f9619d437..b040a0b58 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 @@ -60,6 +60,7 @@ class SecurityConfig(private val webClient: WebClient) { //otc .pathMatchers("/admin/**").hasAuthority("ROLE_admin") .pathMatchers(HttpMethod.GET, "/otc/**").permitAll() + .pathMatchers(HttpMethod.PUT, "/otc/rate").hasAnyAuthority("ROLE_rate_bot","ROLE_admin") .pathMatchers(HttpMethod.PUT, "/otc/**").hasAuthority("ROLE_admin") .pathMatchers(HttpMethod.POST, "/otc/**").hasAuthority("ROLE_admin") .pathMatchers(HttpMethod.DELETE, "/otc/**").hasAuthority("ROLE_admin")