Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,12 @@
package co.nilin.opex.api.core.inout

import java.math.BigDecimal

data class AdminTransferReserveRequest(
val sourceSymbol: String,
val sourceAmount: BigDecimal,
val destSymbol: String,
val destAmount: BigDecimal? = null,
val rate: BigDecimal? = null,
val receiverUuid: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class TransferCategory {
WITHDRAW_REJECT,
WITHDRAW_CANCEL,
PURCHASE_FINALIZED,
IMPERSONATED_PURCHASE_FINALIZED,
WITHDRAW_MANUALLY,
ORDER_CREATE,
ORDER_CANCEL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum class UserTransactionCategory {
WITHDRAW,
FEE,
SWAP,
IMPERSONATED_SWAP,
REFERRAL_COMMISSION,
REFERRAL_KYC_REWARD,
REFERENT_COMMISSION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,12 @@ interface WalletProxy {
suspend fun deleteGateway(token: String, gatewayUuid: String, currencySymbol: String)

suspend fun submitDepositWebhook(request: DepositWebhookRequest, signature: String): DepositWebhookResponse

suspend fun reserveSwapByAdmin(token: String, request: AdminTransferReserveRequest): ReservedTransferResponse
suspend fun finalizeSwapByAdmin(
token: String,
reserveUuid: String,
description: String?,
transferRef: String?
): TransferResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package co.nilin.opex.api.ports.opex.controller

import co.nilin.opex.api.core.inout.AdminTransferReserveRequest
import co.nilin.opex.api.core.inout.ReservedTransferResponse
import co.nilin.opex.api.core.inout.TransferResult
import co.nilin.opex.api.core.spi.WalletProxy
import co.nilin.opex.api.ports.opex.util.jwtAuthentication
import co.nilin.opex.api.ports.opex.util.tokenValue
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/opex/v1/admin/swap")
@Tag(name = "Admin Swap", description = "Admin impersonated swap reserve and finalize operations.")
class AdminSwapController(
val walletProxy: WalletProxy
) {
@PostMapping("/reserve")
@Operation(
summary = "Admin Reserve Swap",
description = """POST /opex/v1/admin/swap/reserve.
Security: Bearer admin-token required. Requires authenticated admin JWT.
Behavior: Admin can reserve swap on behalf of a user by specifying custom rate or amounts.""",
security = [SecurityRequirement(name = "bearerAuth")],
responses = [
ApiResponse(
responseCode = "200",
description = "Successful response.",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = ReservedTransferResponse::class)
)]
),
ApiResponse(
responseCode = "401",
description = "Unauthorized. Bearer token is missing, invalid, or expired.",
content = [Content()]
),
ApiResponse(
responseCode = "403",
description = "Forbidden. User does not have admin permissions.",
content = [Content()]
)
]
)
suspend fun reserve(
@RequestBody request: AdminTransferReserveRequest,
@Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): ReservedTransferResponse {
return walletProxy.reserveSwapByAdmin(securityContext.jwtAuthentication().tokenValue(), request)
}

@PostMapping("/finalize/{reserveUuid}")
@Operation(
summary = "Admin Finalize Transfer",
description = """POST /opex/v1/admin/swap/finalize/{reserveUuid}.
Security: Bearer admin-token required. Requires authenticated admin JWT.
Behavior: Finalizes a reserved swap on behalf of the user.""",
security = [SecurityRequirement(name = "bearerAuth")],
responses = [
ApiResponse(
responseCode = "200",
description = "Successful response.",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = TransferResult::class)
)]
),
ApiResponse(
responseCode = "401",
description = "Unauthorized. Bearer token is missing, invalid, or expired.",
content = [Content()]
),
ApiResponse(
responseCode = "403",
description = "Forbidden. User does not have admin permissions.",
content = [Content()]
)
]
)
suspend fun finalizeTransfer(
@Parameter(
name = "reserveUuid",
description = "Swap reserve UUID returned by reserve endpoint.",
required = true
)
@PathVariable reserveUuid: String,
@Parameter(name = "description", description = "Optional transfer description.", required = false)
@RequestParam description: String?,
@Parameter(name = "transferRef", description = "Optional external transfer reference.", required = false)
@RequestParam transferRef: String?,
@Parameter(hidden = true)
@CurrentSecurityContext securityContext: SecurityContext
): TransferResult {
return walletProxy.finalizeSwapByAdmin(
securityContext.jwtAuthentication().tokenValue(),
reserveUuid,
description,
transferRef
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,7 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
}
.awaitBodilessEntity()
}

override suspend fun submitDepositWebhook(
request: DepositWebhookRequest,
signature: String
Expand All @@ -1107,5 +1108,40 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
.awaitSingle()
}
}

override suspend fun reserveSwapByAdmin(
token: String,
request: AdminTransferReserveRequest
): ReservedTransferResponse {
return webClient.post()
.uri("$baseUrl/admin/v1/transfer/reserve")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.body(Mono.just(request))
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono<ReservedTransferResponse>()
.awaitFirstOrElse { throw OpexError.BadRequest.exception() }
}

override suspend fun finalizeSwapByAdmin(
token: String,
reserveUuid: String,
description: String?,
transferRef: String?
): TransferResult {
return webClient.post()
.uri("$baseUrl/admin/v1/transfer/${reserveUuid}") {
it.queryParam("description", description)
it.queryParam("transferRef", transferRef)
it.build()
}
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
.retrieve()
.onStatus({ t -> t.isError }, { it.createException() })
.bodyToMono<TransferResult>()
.awaitFirstOrElse { throw OpexError.BadRequest.exception() }
}
}

Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package co.nilin.opex.wallet.app.controller

import co.nilin.opex.wallet.app.dto.AdminTransferReserveRequest
import co.nilin.opex.wallet.app.dto.ReservedTransferResponse
import co.nilin.opex.wallet.app.dto.UserSwapTransactionRequest
import co.nilin.opex.wallet.app.dto.UserTransactionRequest
import co.nilin.opex.wallet.app.service.TransferService
import co.nilin.opex.wallet.core.inout.AdminSwapResponse
import co.nilin.opex.wallet.core.inout.TransferResult
import co.nilin.opex.wallet.core.model.WalletType
import co.nilin.opex.wallet.core.spi.ReservedTransferManager
import io.swagger.annotations.ApiResponse
import io.swagger.annotations.Example
import io.swagger.annotations.ExampleProperty
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
Expand All @@ -23,6 +23,8 @@ class AdvancedTransferAdminController {
@Autowired
lateinit var reservedTransferManager: ReservedTransferManager

@Autowired
lateinit var transferService: TransferService

@PostMapping("/admin/v1/swap/history")
@ApiResponse(
Expand Down Expand Up @@ -63,4 +65,32 @@ class AdvancedTransferAdminController {
)
}
}

@PostMapping("/admin/v1/transfer/reserve")
suspend fun reserve(@RequestBody request: AdminTransferReserveRequest): ReservedTransferResponse {
return transferService.reserveTransferByAdmin(
request.sourceSymbol,
request.destSymbol,
request.receiverUuid,
WalletType.MAIN,
request.receiverUuid,
WalletType.MAIN,
request.sourceAmount,
request.destAmount,
request.rate
)
}

@PostMapping("/admin/v1/transfer/{reserveUuid}")
suspend fun finalizeTransfer(
@PathVariable reserveUuid: String,
@RequestParam description: String?,
@RequestParam transferRef: String?,
): TransferResult {
return transferService.advanceTransferByAdmin(
reserveUuid,
description,
transferRef
).transferResult
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.nilin.opex.wallet.app.dto

import java.math.BigDecimal

data class AdminTransferReserveRequest(
val sourceSymbol: String,
val sourceAmount: BigDecimal,
val destSymbol: String,
val destAmount: BigDecimal? = null,
val rate: BigDecimal? = null,
val receiverUuid: String,
)
Loading
Loading