Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5abf42d
Update services
AmirRajabii Dec 10, 2025
7958e1f
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 12, 2025
8c8e1f2
Drop address regx column from chain table
fatemeh-i Dec 13, 2025
476e483
Separate user services
fatemeh-i Dec 15, 2025
d3a5a11
Set allowed audience for resources
fatemeh-i Dec 16, 2025
abd24c8
Check audience and issuer in any security configuration
fatemeh-i Dec 16, 2025
ae21d0c
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 17, 2025
c8ab94e
Read the token issuer url from the env
fatemeh-i Dec 17, 2025
569ff5a
Develop resend otp in login flow
fatemeh-i Dec 21, 2025
51e749b
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 21, 2025
e409909
Replace the exchange approach with bootstrap grant type in login flow
fatemeh-i Dec 23, 2025
55f87b5
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 23, 2025
4746ba7
Send login event in direct grant
fatemeh-i Dec 23, 2025
070c3a7
Remove offline access scope in normal access token
fatemeh-i Dec 24, 2025
5ae59cc
Merge branch 'update-login-flow' of https://github.com/opexdev/core i…
fatemeh-i Dec 24, 2025
7392454
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 24, 2025
72a2a41
Change the request authentication flow in the flow of thirdparty Api …
fatemeh-i Dec 28, 2025
e0e0cbf
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 28, 2025
722171b
Adjust authorization in api controller
fatemeh-i Dec 28, 2025
a8319b3
Read the api crypto key from the valut
fatemeh-i Dec 28, 2025
31f25c6
Wrap the otc services behind the api module
fatemeh-i Dec 29, 2025
fa66d14
Merge branch 'dev' of https://github.com/opexdev/core into update-log…
fatemeh-i Dec 29, 2025
3a7159a
Adjust the code style
fatemeh-i Dec 29, 2025
79ff820
Adjust the code style
fatemeh-i Dec 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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<CurrencyExchangeRate>)
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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<ForbiddenPair>?
)
Original file line number Diff line number Diff line change
@@ -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<Rate>?
)
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package co.nilin.opex.api.core.inout.otc

data class Symbols(var symbols: List<String>?)
Original file line number Diff line number Diff line change
@@ -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<CurrencyPrice>
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,19 @@ 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")
.pathMatchers(HttpMethod.PUT, "/opex/v1/withdraw").hasAuthority("PERM_withdraw:write")
.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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CurrencyPrice> {
return rateProxy.getPrice(unit)
}
}
Original file line number Diff line number Diff line change
@@ -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<String, ActivityTotals> {
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<String, ActivityTotals>(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
}

}
Loading
Loading