From d812b1cc5f6bf2992c09abd564dea3c3e3368a5d Mon Sep 17 00:00:00 2001 From: fatemeh imanipour Date: Sat, 9 May 2026 16:17:00 +0330 Subject: [PATCH 1/5] Add interceptors dependency to all related modules --- accountant/accountant-app/pom.xml | 4 ++++ accountant/pom.xml | 5 +++++ auth-gateway/auth-gateway-app/pom.xml | 4 ++++ bc-gateway/bc-gateway-app/pom.xml | 8 ++++++++ device-management/device-management-app/pom.xml | 4 ++++ device-management/pom.xml | 5 +++++ docker-compose.yml | 4 ++-- matching-gateway/matching-gateway-app/pom.xml | 4 ++++ otp/otp-app/pom.xml | 4 ++++ profile/pom.xml | 5 +++++ profile/profile-app/pom.xml | 4 ++++ user-management/pom.xml | 5 +++++ 12 files changed, 54 insertions(+), 2 deletions(-) diff --git a/accountant/accountant-app/pom.xml b/accountant/accountant-app/pom.xml index 67649ff75..f56aee06a 100644 --- a/accountant/accountant-app/pom.xml +++ b/accountant/accountant-app/pom.xml @@ -51,6 +51,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + org.springframework.cloud spring-cloud-starter-vault-config diff --git a/accountant/pom.xml b/accountant/pom.xml index 6eded39f6..edce57a88 100644 --- a/accountant/pom.xml +++ b/accountant/pom.xml @@ -82,6 +82,11 @@ error-handler ${error-hanlder.version} + + co.nilin.opex.utility + interceptors + ${interceptor.version} + org.springframework.cloud spring-cloud-dependencies diff --git a/auth-gateway/auth-gateway-app/pom.xml b/auth-gateway/auth-gateway-app/pom.xml index 0d31ffddd..0ff3a5ade 100644 --- a/auth-gateway/auth-gateway-app/pom.xml +++ b/auth-gateway/auth-gateway-app/pom.xml @@ -80,6 +80,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + com.auth0 java-jwt diff --git a/bc-gateway/bc-gateway-app/pom.xml b/bc-gateway/bc-gateway-app/pom.xml index 2bdccb667..0046a178b 100644 --- a/bc-gateway/bc-gateway-app/pom.xml +++ b/bc-gateway/bc-gateway-app/pom.xml @@ -138,6 +138,14 @@ micrometer-registry-prometheus runtime + + co.nilin.opex.utility + interceptors + + + co.nilin.opex.utility + error-handler + diff --git a/device-management/device-management-app/pom.xml b/device-management/device-management-app/pom.xml index 82e41ff4e..ff9b70048 100644 --- a/device-management/device-management-app/pom.xml +++ b/device-management/device-management-app/pom.xml @@ -78,6 +78,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + com.zaxxer HikariCP diff --git a/device-management/pom.xml b/device-management/pom.xml index d24596637..4800bfa7c 100644 --- a/device-management/pom.xml +++ b/device-management/pom.xml @@ -56,6 +56,11 @@ error-handler ${error-hanlder.version} + + co.nilin.opex.utility + interceptors + ${interceptor.version} + co.nilin.opex.device-management device-management-core diff --git a/docker-compose.yml b/docker-compose.yml index 63535e6e2..b9078536e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -546,8 +546,8 @@ services: - SMTP_USER=${SMTP_USER} - SMTP_PASS=${SMTP_PASS} - SMTP_FROM=${SMTP_FROM} - - SMTP_SOCKS_HOST=${SMTP_SOCKS_HOST} - - SMTP_SOCKS_PORT=${SMTP_SOCKS_PORT} + - SMTP_PROXY_HOST=${SMTP_PROXY_HOST} + - SMTP_PROXY_PORT=${SMTP_PROXY_PORT} - SMTP_PROXY_ENABLED=${SMTP_PROXY_ENABLED} - TOKEN_ISSUER_URL=${KC_ISSUER_URL} - OTP_CODE_RESPONSE_ENABLED=${OTP_CODE_RESPONSE_ENABLED} diff --git a/matching-gateway/matching-gateway-app/pom.xml b/matching-gateway/matching-gateway-app/pom.xml index 24e94ab58..86d029320 100644 --- a/matching-gateway/matching-gateway-app/pom.xml +++ b/matching-gateway/matching-gateway-app/pom.xml @@ -77,6 +77,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + io.mockk mockk diff --git a/otp/otp-app/pom.xml b/otp/otp-app/pom.xml index 1d3d72124..49cb4f0c0 100644 --- a/otp/otp-app/pom.xml +++ b/otp/otp-app/pom.xml @@ -87,6 +87,10 @@ co.nilin.opex.utility error-handler + + co.nilin.opex.utility + interceptors + io.mockk mockk diff --git a/profile/pom.xml b/profile/pom.xml index 7ac615097..ffd3ab564 100644 --- a/profile/pom.xml +++ b/profile/pom.xml @@ -66,6 +66,11 @@ logging-handler ${logging-handler.version} + + co.nilin.opex.utility + error-handler + ${error-hanlder.version} + co.nilin.opex.utility interceptors diff --git a/profile/profile-app/pom.xml b/profile/profile-app/pom.xml index 685459a22..2b2412044 100644 --- a/profile/profile-app/pom.xml +++ b/profile/profile-app/pom.xml @@ -98,6 +98,10 @@ org.springframework.cloud spring-cloud-starter-vault-config + + co.nilin.opex.utility + error-handler + co.nilin.opex.utility interceptors diff --git a/user-management/pom.xml b/user-management/pom.xml index 0c0b0c5a4..fbb1146c0 100644 --- a/user-management/pom.xml +++ b/user-management/pom.xml @@ -49,6 +49,11 @@ error-handler ${error-hanlder.version} + + co.nilin.opex.utility + interceptors + ${interceptor.version} + co.nilin.opex.auth user-management-core From 8f9b6353c560f40ed2d22a1ec1099fecff17e849 Mon Sep 17 00:00:00 2001 From: fatemeh imanipour Date: Thu, 28 May 2026 14:44:10 +0330 Subject: [PATCH 2/5] Develop the webhook to process incoming deposit requests --- .../api/core/inout/DepositWebhookHeaders.kt | 5 ++ .../api/core/inout/DepositWebhookRequest.kt | 12 ++++ .../api/core/inout/DepositWebhookResponse.kt | 9 +++ .../co/nilin/opex/api/core/spi/WalletProxy.kt | 2 +- .../controller/DepositWebhookController.kt | 27 ++++++++ .../api/ports/proxy/impl/WalletProxyImpl.kt | 18 ++++++ .../opex/bcgateway/app/config/AppConfig.kt | 15 ----- .../app/controller/ScannerController.kt | 39 +++--------- .../nilin/opex/common/utils/SignVerifier.kt | 58 +++++++++++++++++ .../profile/app/service/ProfileManagement.kt | 3 +- .../core/data/event/ProfileUpdatedEvent.kt | 3 +- .../app/controller/DepositController.kt | 25 +++++++- .../app/controller/WalletOwnerController.kt | 2 +- .../nilin/opex/wallet/app/dto/CurrentUser.kt | 3 +- .../ProfileUpdatedEventListenerImpl.kt | 3 +- .../opex/wallet/app/service/CurrentUser.kt | 3 +- .../opex/wallet/app/service/DepositService.kt | 35 +++++++++++ .../service/FiatScannerSignatureVerifier.kt | 63 +++++++++++++++++++ .../core/inout/DepositWebhookRequest.kt | 14 +++++ .../core/inout/DepositWebhookResponse.kt | 9 +++ .../wallet/core/spi/WalletOwnerManager.kt | 5 +- .../listener/model/ProfileUpdatedEvent.kt | 3 +- .../postgres/impl/WalletOwnerManagerImpl.kt | 21 ++++++- .../ports/postgres/model/WalletOwnerModel.kt | 2 + .../V12__update_wallet_owner_table.sql | 4 ++ 25 files changed, 319 insertions(+), 64 deletions(-) create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt create mode 100644 api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositWebhookController.kt create mode 100644 common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt create mode 100644 wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt create mode 100644 wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt create mode 100644 wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt create mode 100644 wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V12__update_wallet_owner_table.sql diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt new file mode 100644 index 000000000..f89bb8af6 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.api.core.inout + +object DepositWebhookHeaders { + const val SIGNATURE = "X-Fiat-Scanner-Signature" +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt new file mode 100644 index 000000000..9cde92437 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.api.core.inout + +import java.math.BigDecimal + +data class DepositWebhookRequest( + val referenceNumber: String, + val depositNumber: String, + val symbol: String, + val amount: BigDecimal, + val payId: String, + val date: Long +) diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt new file mode 100644 index 000000000..659cc3d3f --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.api.core.inout + +data class DepositWebhookResponse( + val referenceNumber: String? = null, + val status: String, + val duplicate: Boolean = false, + val message: String? = null +) + 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 402f8bd21..f90018a32 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 @@ -261,5 +261,5 @@ interface WalletProxy { suspend fun deleteGateway(token: String, gatewayUuid: String, currencySymbol: String) - + suspend fun submitDepositWebhook(request: DepositWebhookRequest, signature: String): DepositWebhookResponse } \ 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/DepositWebhookController.kt b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositWebhookController.kt new file mode 100644 index 000000000..12180c572 --- /dev/null +++ b/api/api-ports/api-opex-rest/src/main/kotlin/co/nilin/opex/api/ports/opex/controller/DepositWebhookController.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.api.ports.opex.controller + +import co.nilin.opex.api.core.inout.DepositWebhookRequest +import co.nilin.opex.api.core.inout.DepositWebhookResponse +import co.nilin.opex.api.core.inout.DepositWebhookHeaders +import co.nilin.opex.api.core.spi.WalletProxy +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/v1/deposit") +class DepositWebhookController( + private val walletProxy: WalletProxy +) { + @PostMapping( + "/webhook" + ) + suspend fun submit( + @RequestHeader(DepositWebhookHeaders.SIGNATURE) sign: String, @RequestBody body: DepositWebhookRequest + ): DepositWebhookResponse { + return walletProxy.submitDepositWebhook( + body, + sign + ) + } +} + + 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 25d7b969f..775a54f12 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 @@ -1110,5 +1110,23 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC } .awaitBodilessEntity() } + override suspend fun submitDepositWebhook( + request: DepositWebhookRequest, + signature: String + ): DepositWebhookResponse { + logger.info("proxying deposit webhook to wallet") + + return withContext(ProxyDispatchers.wallet) { + webClient.post() + .uri("$baseUrl/v1/deposit/webhook") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(DepositWebhookHeaders.SIGNATURE, signature) + .bodyValue(request) + .retrieve() + .bodyToMono() + .awaitSingle() + } + } } diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt index 7c2b0e6fb..6c326f929 100644 --- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt +++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/config/AppConfig.kt @@ -48,20 +48,5 @@ class AppConfig(private val resourceLoader: ResourceLoader) { adminKafkaEventListener.addEventListener(adminEventListener) } - @Bean("webhookPublicKey") - fun webhookPublicKey(): PublicKey { - val publicKeyString = resourceLoader.getResource("classpath:scanner-public.pem").inputStream - .readAllBytes() - .toString(Charsets.UTF_8) - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("\n", "") - .replace("\r", "") - - val keyBytes = Base64.getDecoder().decode(publicKeyString) - val keySpec = X509EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - return keyFactory.generatePublic(keySpec) - } } diff --git a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt index fd89f0b83..7c79f00cf 100644 --- a/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt +++ b/bc-gateway/bc-gateway-app/src/main/kotlin/co/nilin/opex/bcgateway/app/controller/ScannerController.kt @@ -3,20 +3,12 @@ package co.nilin.opex.bcgateway.app.controller import co.nilin.opex.bcgateway.core.api.WalletSyncService import co.nilin.opex.bcgateway.core.model.Transfer import co.nilin.opex.bcgateway.core.model.Wallet -import co.nilin.opex.common.OpexError -import co.nilin.opex.utility.error.data.OpexException +import co.nilin.opex.common.utils.SignVerifier import com.fasterxml.jackson.databind.ObjectMapper import org.slf4j.LoggerFactory import org.springframework.core.io.ResourceLoader -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.math.BigDecimal -import java.security.PublicKey -import java.security.Signature -import java.util.* data class WebhookBody( val txId: String, @@ -33,7 +25,7 @@ data class WebhookBody( @RestController @RequestMapping("/scanner") class ScannerController( - private val publicKey: PublicKey, + private val resourceLoader: ResourceLoader, private val mapper: ObjectMapper, private val service: WalletSyncService ) { @@ -42,30 +34,13 @@ class ScannerController( @PostMapping("/webhook") suspend fun webhook(@RequestHeader("X-Signature") sign: String, @RequestBody body: WebhookBody) { - verifySignature(sign, body) + val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream + .readAllBytes() + .toString(Charsets.UTF_8) + SignVerifier().verify("SHA256withRSA", publicKeyRawStr, mapper.writeValueAsString(body), sign) logger.info("Webhook received for address ${body.address}, amount ${body.amount}") service.sendTransfer(with(body) { Transfer(txId, Wallet(address, memo), isToken, amount, chain, tokenAddress) }) } - private fun verifySignature(sign: String, request: WebhookBody) { - try { - logger.info("Verifying signature for address ${request.address}") - val reqStr = mapper.writeValueAsString(request) - val decodedSign = Base64.getDecoder().decode(sign) - val verifier = Signature.getInstance("SHA256withRSA").apply { - initVerify(publicKey) - update(reqStr.toByteArray()) - } - if (!verifier.verify(decodedSign)) { - logger.warn("Signature is not valid!") - throw OpexError.Forbidden.exception() - } - } catch (e: OpexException) { - throw e - } catch (e: Exception) { - logger.error("Unable to verify signature", e) - throw OpexError.InternalServerError.exception() - } - } } \ No newline at end of file diff --git a/common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt b/common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt new file mode 100644 index 000000000..288514a47 --- /dev/null +++ b/common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt @@ -0,0 +1,58 @@ +package co.nilin.opex.common.utils + + +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 + +class SignVerifier{ + + fun verify(signatureAlgorithm: String , publicKeyPem: String, payload: String, signatureBase64: String) { + val signatureBytes = try { + Base64.getDecoder().decode(signatureBase64) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature encoding") + } + + val publicKey = try { + val normalizedPem = publicKeyPem + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replace("\\s".toRegex(), "") + + val keyBytes = Base64.getDecoder().decode(normalizedPem) + val keySpec = X509EncodedKeySpec(keyBytes) + + KeyFactory + .getInstance(resolveKeyFactoryAlgorithm(signatureAlgorithm)) + .generatePublic(keySpec) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid fiat scanner public key") + } + + val verified = try { + Signature.getInstance(signatureAlgorithm).apply { + initVerify(publicKey) + update(payload.toByteArray(Charsets.UTF_8)) + }.verify(signatureBytes) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Fiat scanner signature verification failed") + } + + if (!verified) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature") + } + } + + private fun resolveKeyFactoryAlgorithm(algorithm: String): String { + return when { + algorithm.equals("Ed25519", ignoreCase = true) -> "Ed25519" + algorithm.contains("RSA", ignoreCase = true) -> "RSA" + algorithm.contains("ECDSA", ignoreCase = true) -> "EC" + else -> "RSA" + } + } +} \ No newline at end of file diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt index 74df80086..daaa1653c 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt @@ -161,7 +161,8 @@ class ProfileManagement( ProfileUpdatedEvent( userId = userId, firstName = request.firstName, - lastName = request.lastName + lastName = request.lastName, + identifier = request.identifier ) ) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt index 93ad3220d..ad7e6394f 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/event/ProfileUpdatedEvent.kt @@ -5,5 +5,6 @@ data class ProfileUpdatedEvent( var firstName: String? = null, var lastName: String? = null, var email: String? = null, - var mobile: String? = null + var mobile: String? = null, + var identifier: String? = null ) diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt index 0536ac9b8..9bd16151f 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt @@ -1,17 +1,18 @@ package co.nilin.opex.wallet.app.controller +import co.nilin.opex.common.utils.SignVerifier import co.nilin.opex.wallet.app.dto.DepositHistoryRequest import co.nilin.opex.wallet.app.service.DepositService import co.nilin.opex.wallet.app.utils.asLocalDateTime -import co.nilin.opex.wallet.core.inout.DepositResponse -import co.nilin.opex.wallet.core.inout.TransactionSummary -import co.nilin.opex.wallet.core.inout.TransferResult +import co.nilin.opex.wallet.core.inout.* import co.nilin.opex.wallet.core.model.DepositType import co.nilin.opex.wallet.core.model.WalletType +import com.fasterxml.jackson.databind.ObjectMapper import io.swagger.annotations.ApiResponse import io.swagger.annotations.Example import io.swagger.annotations.ExampleProperty +import org.springframework.core.io.ResourceLoader import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.* @@ -24,6 +25,8 @@ import java.time.ZoneId @RequestMapping class DepositController( private val depositService: DepositService, + private val resourceLoader: ResourceLoader, + private val mapper: ObjectMapper, ) { @PostMapping("/v1/deposit/history") @@ -114,6 +117,22 @@ class DepositController( limit, ) } + + @PostMapping("/v1/deposit/webhook") + suspend fun submit( + @RequestBody request: DepositWebhookRequest, + @RequestHeader(DepositWebhookHeaders.SIGNATURE) signature: String + ): DepositWebhookResponse { + val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream + .readAllBytes() + .toString(Charsets.UTF_8) + SignVerifier().verify("", publicKeyRawStr, mapper.writeValueAsString(request), signature) + return depositService.processExternalDeposit(request) + } + + object DepositWebhookHeaders { + const val SIGNATURE = "X-Fiat-Scanner-Signature" + } } 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 fc69f34ae..571db4d57 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 @@ -41,7 +41,7 @@ class WalletOwnerController( val owner = walletOwnerManager.findWalletOwner(uuid) ?: run { if (currentUserProvider.getCurrentUser()?.uuid.equals(uuid) && environment.activeProfiles.contains("otc")) walletOwnerManager.createWalletOwner( - uuid, currentUserProvider.getCurrentUser()?.mobile ?: "not set", "" + uuid, currentUserProvider.getCurrentUser()?.mobile ?: "not set", "", currentUserProvider.getCurrentUser()?.identityId ) throw OpexError.WalletOwnerNotFound.exception() } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt index b8fc9f4a1..772a10b3b 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/dto/CurrentUser.kt @@ -7,5 +7,6 @@ data class CurrentUser( val fullName: String?, val mobile: String?, val roles: List, - val level: String? + val level: String?, + val identityId: String? ) \ No newline at end of file diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt index 767167bc5..f6de5d210 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/listener/ProfileUpdatedEventListenerImpl.kt @@ -19,8 +19,7 @@ class ProfileUpdatedEventListenerImpl(private val walletOwnerManager: WalletOwne if (!event.firstName.isNullOrBlank() && !event.lastName.isNullOrBlank()) { runBlocking { logger.info("Incoming ProfileUpdated event $event") - walletOwnerManager.updateWalletOwnerName(event.userId, "${event.firstName} ${event.lastName}") - + walletOwnerManager.updateWalletOwnerName(event.userId, "${event.firstName} ${event.lastName}", event.identifier) } } else Unit } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt index 5b4f76b55..055b4f770 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/CurrentUser.kt @@ -26,7 +26,8 @@ class CurrentUserProvider { ).joinToString(" "), mobile = jwt.getClaimAsString("mobile"), roles = jwt.getClaimAsStringList("roles") ?: emptyList(), - level = jwt.getClaimAsString("level") + level = jwt.getClaimAsString("level"), + identityId = jwt.getClaimAsString("identity_id") ) } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt index 7d758e1f5..c16539da8 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt @@ -107,6 +107,41 @@ class DepositService( transferMethod = TransferMethod.MANUALLY ) } + // ------------------------------------------------------------------------- + // Manual Deposit + // ------------------------------------------------------------------------- + + @Transactional + suspend fun processExternalDeposit( + request: DepositWebhookRequest + ): DepositWebhookResponse { + + logger.info( + "New incoming deposit request : $ to ${request.externalIdentifier} on ${request.symbol} at ${LocalDateTime.now()}" + ) + + val receiverUuid = walletOwnerManager.findWalletOwnerByExternalIdentifier(request.externalIdentifier)?.uuid + ?: throw OpexError.BadRequest.exception("Identifier ${request.externalIdentifier} not fount") + + with(request) { + deposit( + symbol = symbol, + receiverUuid = receiverUuid, + receiverWalletType = WalletType.MAIN, + senderUuid = walletOwnerManager.systemUuid, + amount = amount, + description = "Transfer to $depositNumber at $date, payId: $externalIdentifier", + transferRef = referenceNumber, + chain = null, + attachment = null, + depositType = DepositType.OFF_CHAIN, + gatewayUuid = null, + transferMethod = TransferMethod.SHEBA + ) + } + return DepositWebhookResponse(request.referenceNumber, DepositStatus.DONE.name, false) + } + // ------------------------------------------------------------------------- // Core Deposit diff --git a/wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt b/wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt new file mode 100644 index 000000000..70083f33d --- /dev/null +++ b/wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt @@ -0,0 +1,63 @@ +package co.nilin.opex.wallet.app.service + +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 + +@Service +class FiatScannerSignatureVerifier( + @Value("\${app.fiat-scanner.public-key}") private val publicKeyPem: String, + @Value("\${app.fiat-scanner.signature-algorithm:SHA256withRSA}") private val signatureAlgorithm: String +) { + + fun verify(payload: String, signatureBase64: String) { + val signatureBytes = try { + Base64.getDecoder().decode(signatureBase64) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature encoding") + } + + val publicKey = try { + val normalizedPem = publicKeyPem + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replace("\\s".toRegex(), "") + + val keyBytes = Base64.getDecoder().decode(normalizedPem) + val keySpec = X509EncodedKeySpec(keyBytes) + + KeyFactory + .getInstance(resolveKeyFactoryAlgorithm(signatureAlgorithm)) + .generatePublic(keySpec) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid fiat scanner public key") + } + + val verified = try { + Signature.getInstance(signatureAlgorithm).apply { + initVerify(publicKey) + update(payload.toByteArray(Charsets.UTF_8)) + }.verify(signatureBytes) + } catch (ex: Exception) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Fiat scanner signature verification failed") + } + + if (!verified) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature") + } + } + + private fun resolveKeyFactoryAlgorithm(algorithm: String): String { + return when { + algorithm.equals("Ed25519", ignoreCase = true) -> "Ed25519" + algorithm.contains("RSA", ignoreCase = true) -> "RSA" + algorithm.contains("ECDSA", ignoreCase = true) -> "EC" + else -> "RSA" + } + } +} \ No newline at end of file diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt new file mode 100644 index 000000000..218c47a56 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.wallet.core.inout + +import java.math.BigDecimal + +data class DepositWebhookRequest( + val referenceNumber: String, + val depositNumber: String, + val symbol: String, + val amount: BigDecimal, + val externalIdentifier: String, + val uuid: String?, + val date: Long +) + diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt new file mode 100644 index 000000000..c6c0b9d42 --- /dev/null +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookResponse.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.wallet.core.inout + +data class DepositWebhookResponse( + val referenceNumber: String, + val status: String, + val duplicate: Boolean = false, + val message: String? = null +) + diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt index abeb78825..9595b0051 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/spi/WalletOwnerManager.kt @@ -10,7 +10,8 @@ interface WalletOwnerManager { suspend fun isDepositAllowed(owner: WalletOwner, amount: Amount): Boolean suspend fun isWithdrawAllowed(owner: WalletOwner, amount: Amount): Boolean suspend fun findWalletOwner(uuid: String): WalletOwner? - suspend fun createWalletOwner(uuid: String, title: String, userLevel: String): WalletOwner + suspend fun findWalletOwnerByExternalIdentifier(externalIdentifier: String): WalletOwner? + suspend fun createWalletOwner(uuid: String, title: String, userLevel: String, externalIdentifier: String?=null): WalletOwner suspend fun findAllWalletOwners(): List - suspend fun updateWalletOwnerName(uuid: String, name: String) + suspend fun updateWalletOwnerName(uuid: String, name: String, externalIdentifier: String?) } diff --git a/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt b/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt index 918a42309..ab5575714 100644 --- a/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt +++ b/wallet/wallet-ports/wallet-eventlistener-kafka/src/main/kotlin/co/nilin/opex/wallet/ports/kafka/listener/model/ProfileUpdatedEvent.kt @@ -5,5 +5,6 @@ data class ProfileUpdatedEvent( var firstName: String? = null, var lastName: String? = null, var email: String? = null, - var mobile: String? = null + var mobile: String? = null, + var identifier: String? = null, ) : AuthEvent() diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt index b4811a52c..1bb6041a1 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt @@ -131,8 +131,22 @@ class WalletOwnerManagerImpl( return walletOwnerRepository.findByUuid(uuid).awaitFirstOrNull()?.toPlainObject() } - override suspend fun createWalletOwner(uuid: String, title: String, userLevel: String): WalletOwner { - return walletOwnerRepository.save(WalletOwnerModel(null, uuid, title, userLevel)).awaitFirst().toPlainObject() + override suspend fun createWalletOwner( + uuid: String, + title: String, + userLevel: String, + externalIdentifier: String? + ): WalletOwner { + return walletOwnerRepository.save( + WalletOwnerModel( + null, + uuid, + title, + userLevel, + externalIdentifier = externalIdentifier + ) + ) + .awaitFirst().toPlainObject() } override suspend fun findAllWalletOwners(): List { @@ -152,10 +166,11 @@ class WalletOwnerManagerImpl( } } - override suspend fun updateWalletOwnerName(uuid: String, name: String) { + override suspend fun updateWalletOwnerName(uuid: String, name: String, externalIdentifier: String?) { val owner = walletOwnerRepository.findByUuid(uuid).awaitFirstOrNull() if (owner != null) { owner.title = owner.title.split('|')[0] + "|" + name + owner.externalIdentifier = externalIdentifier walletOwnerRepository.save(owner).awaitFirstOrNull() ?: logger.warn("Failed to update wallet owner name for UUID: $uuid") } else { diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt index a047a7f62..b49c16f3c 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/WalletOwnerModel.kt @@ -16,4 +16,6 @@ data class WalletOwnerModel( var isWithdrawAllowed: Boolean = true, @Column("deposit_allowed") var isDepositAllowed: Boolean = true, + @Column("external_identifier") + var externalIdentifier: String?=null ) \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V12__update_wallet_owner_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V12__update_wallet_owner_table.sql new file mode 100644 index 000000000..70326c8fe --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V12__update_wallet_owner_table.sql @@ -0,0 +1,4 @@ +ALTER TABLE wallet_owner add COLUMN external_identifier VARCHAR(100); +CREATE UNIQUE INDEX wallet_owner_external_identifier + ON wallet_owner (external_identifier) + WHERE external_identifier IS NOT NULL; \ No newline at end of file From 73a80bb3635cf33befa178c38aa84e3abe6ee5f3 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 30 May 2026 18:58:12 +0330 Subject: [PATCH 3/5] fix deposit webhook service --- .../opex/api/core/inout/DepositWebhookHeaders.kt | 2 +- .../opex/api/core/inout/DepositWebhookRequest.kt | 2 +- .../api/ports/binance/config/SecurityConfig.kt | 1 + device-management/pom.xml | 2 +- .../opex/wallet/app/config/SecurityConfig.kt | 1 + .../wallet/app/controller/DepositController.kt | 4 ++-- .../opex/wallet/app/service/DepositService.kt | 15 ++++++++++----- .../src/main/resources/scanner-public.pem | 9 +++++++++ .../wallet/core/inout/DepositWebhookRequest.kt | 1 - .../ports/postgres/dao/WalletOwnerRepository.kt | 1 + .../ports/postgres/impl/WalletOwnerManagerImpl.kt | 4 ++++ .../postgres/model/TerminalLocalizationModel.kt | 4 ++-- .../V13__update_terminal_localization_table.sql | 2 ++ ...ble.sql => V14__update_wallet_owner_table.sql} | 0 .../db/migration/V15__update_deposit_table.sql | 3 +++ 15 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 wallet/wallet-app/src/main/resources/scanner-public.pem create mode 100644 wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql rename wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/{V12__update_wallet_owner_table.sql => V14__update_wallet_owner_table.sql} (100%) create mode 100644 wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt index f89bb8af6..90d628b97 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookHeaders.kt @@ -1,5 +1,5 @@ package co.nilin.opex.api.core.inout object DepositWebhookHeaders { - const val SIGNATURE = "X-Fiat-Scanner-Signature" + const val SIGNATURE = "X-Signature" } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt index 9cde92437..86d5b07a9 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/DepositWebhookRequest.kt @@ -7,6 +7,6 @@ data class DepositWebhookRequest( val depositNumber: String, val symbol: String, val amount: BigDecimal, - val payId: String, + val externalIdentifier: String, val date: Long ) 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 f9bd55dc6..9769b02f0 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 @@ -49,6 +49,7 @@ class SecurityConfig( .pathMatchers(HttpMethod.DELETE, "/v3/order").hasAuthority("PERM_order:write") // Opex endpoints + .pathMatchers("/v1/deposit/webhook").permitAll() .pathMatchers("/opex/v1/admin/transactions/**").hasAnyAuthority("ROLE_monitoring", "ROLE_admin") .pathMatchers("/opex/v1/storage/**").permitAll() .pathMatchers("/opex/v1/web/config/**").permitAll() diff --git a/device-management/pom.xml b/device-management/pom.xml index 4800bfa7c..fc80e43f7 100644 --- a/device-management/pom.xml +++ b/device-management/pom.xml @@ -22,7 +22,7 @@ 2.1.0 3.4.2 2024.0.0 - 1.2.19 + 1.2.25 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 ad6105aea..e10790d8d 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 @@ -31,6 +31,7 @@ class SecurityConfig(private val webClient: WebClient) { fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? { http.csrf().disable() .authorizeExchange() + .pathMatchers("/v1/deposit/webhook").permitAll() .pathMatchers("/balanceOf/**").authenticated() .pathMatchers("/owner/**").authenticated() .pathMatchers("/withdraw").authenticated() diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt index 9bd16151f..be0a67b19 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/controller/DepositController.kt @@ -126,12 +126,12 @@ class DepositController( val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream .readAllBytes() .toString(Charsets.UTF_8) - SignVerifier().verify("", publicKeyRawStr, mapper.writeValueAsString(request), signature) + SignVerifier().verify("SHA512withRSA", publicKeyRawStr, mapper.writeValueAsString(request), signature) return depositService.processExternalDeposit(request) } object DepositWebhookHeaders { - const val SIGNATURE = "X-Fiat-Scanner-Signature" + const val SIGNATURE = "X-Signature" } } diff --git a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt index c16539da8..a07602d98 100644 --- a/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt +++ b/wallet/wallet-app/src/main/kotlin/co/nilin/opex/wallet/app/service/DepositService.kt @@ -33,7 +33,7 @@ class DepositService( ) { private val logger = LoggerFactory.getLogger(DepositService::class.java) - + // ------------------------------------------------------------------------- // Helpers (NO LOGIC CHANGE) // ------------------------------------------------------------------------- @@ -123,7 +123,7 @@ class DepositService( val receiverUuid = walletOwnerManager.findWalletOwnerByExternalIdentifier(request.externalIdentifier)?.uuid ?: throw OpexError.BadRequest.exception("Identifier ${request.externalIdentifier} not fount") - with(request) { + with(request) { deposit( symbol = symbol, receiverUuid = receiverUuid, @@ -136,7 +136,8 @@ class DepositService( attachment = null, depositType = DepositType.OFF_CHAIN, gatewayUuid = null, - transferMethod = TransferMethod.SHEBA + transferMethod = TransferMethod.SHEBA, + persistInvalidDeposit = false ) } return DepositWebhookResponse(request.referenceNumber, DepositStatus.DONE.name, false) @@ -161,6 +162,7 @@ class DepositService( depositType: DepositType, gatewayUuid: String?, transferMethod: TransferMethod?, + persistInvalidDeposit: Boolean = true, ): TransferResult? { logger.info( @@ -200,7 +202,10 @@ class DepositService( depositCommand.status = DepositStatus.INVALID } - traceDepositService.saveDepositInNewTransaction(depositCommand) + if (persistInvalidDeposit) + traceDepositService.saveDepositInNewTransaction(depositCommand) + else + depositPersister.persist(depositCommand) if (!isValid) { return null @@ -256,7 +261,7 @@ class DepositService( ): GatewayData { if (gatewayUuid == null) { - return GatewayData(true, BigDecimal.ZERO, BigDecimal.ZERO, null) + return GatewayData(true, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(Long.MAX_VALUE)) } val gateway = currencyServiceV2 diff --git a/wallet/wallet-app/src/main/resources/scanner-public.pem b/wallet/wallet-app/src/main/resources/scanner-public.pem new file mode 100644 index 000000000..ab3f1c0d5 --- /dev/null +++ b/wallet/wallet-app/src/main/resources/scanner-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAufhMQdywCvA5wVOFosP5 +gkD6O1HjXq/dlcNpEDaReodG/+66hwLESXoWdVw1PnuZqam+DejUkBCxL7f3s0vM +X1gwDTmobMqFS1qdDTsGIglGUTd4o0Ln0x8dHa6NwpHigsruL9fymYLRmLuGPSIj +tip2h1wPZ0CWMzm+Knvb8yurVaXTXRzhHRGOcWR0wUGbluJ762R46DZQK38gsPfK +/slLo0GFcHyv7T+woIX7hOh1Gr3samAspt+jHUU52rXKKn9jfPanyqMt0T6mbDEk +MALiFMqGgkgCgxc5Ni5M8lgP6jjj4PwoN96K6rMF1enFosWOtrpg4XYP/aKuxwla +3wIDAQAB +-----END PUBLIC KEY----- diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt index 218c47a56..c45023fa2 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/inout/DepositWebhookRequest.kt @@ -8,7 +8,6 @@ data class DepositWebhookRequest( val symbol: String, val amount: BigDecimal, val externalIdentifier: String, - val uuid: String?, val date: Long ) diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt index 2425f4511..51ddd018c 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/WalletOwnerRepository.kt @@ -11,4 +11,5 @@ interface WalletOwnerRepository : ReactiveCrudRepository @Query("select * from wallet_owner where uuid = :uuid") fun findByUuid(uuid: String): Mono + fun findByExternalIdentifier(externalIdentifier: String): Mono } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt index 1bb6041a1..0d7329e39 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerImpl.kt @@ -177,4 +177,8 @@ class WalletOwnerManagerImpl( logger.warn("Wallet owner not found for UUID: $uuid") } } + + override suspend fun findWalletOwnerByExternalIdentifier(externalIdentifier: String): WalletOwner? { + return walletOwnerRepository.findByExternalIdentifier(externalIdentifier).awaitFirstOrNull()?.toPlainObject() + } } diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt index bc11d2b81..c1bf7892a 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/model/TerminalLocalizationModel.kt @@ -8,7 +8,7 @@ data class TerminalLocalizationModel( @Id var id: Long? = null, var terminalId: Long, - var description: String?=null, - var owner: String?=null, + var description: String? = null, + var owner: String? = null, var language: String, ) \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql new file mode 100644 index 000000000..0ec5cd86f --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V13__update_terminal_localization_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE terminal_localization + ALTER COLUMN description DROP NOT NULL; \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V12__update_wallet_owner_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V14__update_wallet_owner_table.sql similarity index 100% rename from wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V12__update_wallet_owner_table.sql rename to wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V14__update_wallet_owner_table.sql diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql new file mode 100644 index 000000000..e2cfe0ef6 --- /dev/null +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql @@ -0,0 +1,3 @@ +alter table deposits + add constraint transaction_ref_pk + unique (transaction_ref); \ No newline at end of file From eb84595b93cbb15c4a227dbdcb12064db3f2a274 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sun, 31 May 2026 13:45:44 +0330 Subject: [PATCH 4/5] Update V15__update_deposit_table.sql --- .../db/migration/V15__update_deposit_table.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql index e2cfe0ef6..4d10437ee 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/db/migration/V15__update_deposit_table.sql @@ -1,3 +1,19 @@ +WITH duplicates AS ( + SELECT id + FROM ( + SELECT id, + ROW_NUMBER() OVER ( + PARTITION BY transaction_ref + ORDER BY id + ) rn + FROM deposits + ) t + WHERE rn > 1 +) +UPDATE deposits +SET transaction_ref = id::text || '_' || transaction_ref +WHERE id IN (SELECT id FROM duplicates); + alter table deposits add constraint transaction_ref_pk unique (transaction_ref); \ No newline at end of file From d82efecab7435a3bda7cde5c52173ec08e6b2687 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sun, 31 May 2026 14:11:16 +0330 Subject: [PATCH 5/5] Delete FiatScannerSignatureVerifier.kt --- .../service/FiatScannerSignatureVerifier.kt | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt diff --git a/wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt b/wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt deleted file mode 100644 index 70083f33d..000000000 --- a/wallet/wallet-app/src/test/kotlin/co/nilin/opex/wallet/app/service/FiatScannerSignatureVerifier.kt +++ /dev/null @@ -1,63 +0,0 @@ -package co.nilin.opex.wallet.app.service - -import org.springframework.beans.factory.annotation.Value -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Service -import org.springframework.web.server.ResponseStatusException -import java.security.KeyFactory -import java.security.Signature -import java.security.spec.X509EncodedKeySpec -import java.util.Base64 - -@Service -class FiatScannerSignatureVerifier( - @Value("\${app.fiat-scanner.public-key}") private val publicKeyPem: String, - @Value("\${app.fiat-scanner.signature-algorithm:SHA256withRSA}") private val signatureAlgorithm: String -) { - - fun verify(payload: String, signatureBase64: String) { - val signatureBytes = try { - Base64.getDecoder().decode(signatureBase64) - } catch (ex: Exception) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature encoding") - } - - val publicKey = try { - val normalizedPem = publicKeyPem - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("\\s".toRegex(), "") - - val keyBytes = Base64.getDecoder().decode(normalizedPem) - val keySpec = X509EncodedKeySpec(keyBytes) - - KeyFactory - .getInstance(resolveKeyFactoryAlgorithm(signatureAlgorithm)) - .generatePublic(keySpec) - } catch (ex: Exception) { - throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid fiat scanner public key") - } - - val verified = try { - Signature.getInstance(signatureAlgorithm).apply { - initVerify(publicKey) - update(payload.toByteArray(Charsets.UTF_8)) - }.verify(signatureBytes) - } catch (ex: Exception) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Fiat scanner signature verification failed") - } - - if (!verified) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature") - } - } - - private fun resolveKeyFactoryAlgorithm(algorithm: String): String { - return when { - algorithm.equals("Ed25519", ignoreCase = true) -> "Ed25519" - algorithm.contains("RSA", ignoreCase = true) -> "RSA" - algorithm.contains("ECDSA", ignoreCase = true) -> "EC" - else -> "RSA" - } - } -} \ No newline at end of file