From 85cff6b0d323533df3e52145b08137465736885f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 14:42:09 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../api/common/blockaid/BlockAidApi.kt | 3 +- .../request/SolanaTransactionScanRequest.kt | 2 +- .../response/SolanaTransactionResponse.kt | 58 ++++++++++++++++ .../tangem/data/blockaid/BlockAidMapper.kt | 68 ++++++++++++++++++- .../blockaid/DefaultBlockAidRepository.kt | 25 ++++--- .../blockaid/DefaultBlockAidRepositoryTest.kt | 3 +- .../utils/BlockAidVerificationDelegate.kt | 10 ++- .../walletconnect/model/WcSolanaMethod.kt | 15 +++- .../WcEstimatedWalletChangeUMConverter.kt | 5 +- .../WcSendAndReceiveBlockAidUiConverter.kt | 2 +- 10 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt index 605bc8a391..cdb6ec04e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse import retrofit2.http.Body import retrofit2.http.POST @@ -17,5 +18,5 @@ interface BlockAidApi { suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse @POST("solana/message/scan") - suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse + suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt index 232343cf53..e89428b6a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.common.blockaid.models.response.TransactionMeta @JsonClass(generateAdapter = true) data class SolanaTransactionScanRequest( @Json(name = "encoding") val encoding: String = "base64", - @Json(name = "chain") val chain: String, + @Json(name = "blockchain") val blockchain: String, @Json(name = "method") val method: String, @Json(name = "options") val options: List = listOf("simulation", "validation"), @Json(name = "metadata") val metadata: TransactionMetadata, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt new file mode 100644 index 0000000000..d98f56ee5d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt @@ -0,0 +1,58 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SolanaTransactionResponse( + @Json(name = "result") val result: SolanaTransactionResult, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionResult( + @Json(name = "validation") val validation: SolanaTransactionValidation, + @Json(name = "simulation") val simulation: SolanaTransactionSimulation? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionValidation( + @Json(name = "result_type") val resultType: String, + @Json(name = "description") val description: String?, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionSimulation( + @Json(name = "account_summary") val accountSummary: SolanaTransactionAccountSummary, + @Json(name = "error") val error: String? = null, + @Json(name = "error_details") val errorDetails: String? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionAccountSummary( + @Json(name = "account_assets_diff") + val accountAssetsDiff: List, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionAssetDiff( + @Json(name = "asset_type") val assetType: String, + @Json(name = "asset") val asset: SolanaTransactionAsset, + @Json(name = "in") val inTransfer: SolanaTransferDetail? = null, + @Json(name = "out") val outTransfer: SolanaTransferDetail? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionAsset( + @Json(name = "address") val address: String? = null, + @Json(name = "symbol") val symbol: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "decimals") val decimals: Int? = null, + @Json(name = "type") val type: String? = null, + @Json(name = "logo") val logoUrl: String? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransferDetail( + @Json(name = "value") val amount: String? = null, + @Json(name = "summary") val summary: String? = null, +) \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index 128d1cfd56..e61f8ddd21 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -17,6 +17,9 @@ private const val SUCCESS_STATUS = "Success" private const val DOMAIN_CHECKED_STATUS = "hit" private const val VALIDATION_SAFE_STATUS = "Benign" private const val VALIDATION_WARNING_STATUS = "Warning" +private const val VALIDATION_MALICIOUS_STATUS = "Malicious" + +private const val SOL_ASSET_SYMBOL = "SOL" internal object BlockAidMapper { @@ -28,6 +31,26 @@ internal object BlockAidMapper { } } + fun mapToDomain(from: SolanaTransactionResponse): CheckTransactionResult { + val validation = when (from.result.validation.resultType) { + VALIDATION_SAFE_STATUS -> ValidationResult.SAFE + VALIDATION_WARNING_STATUS -> ValidationResult.WARNING + VALIDATION_MALICIOUS_STATUS -> ValidationResult.UNSAFE + else -> ValidationResult.FAILED_TO_VALIDATE + } + val simulationResponse = from.result.simulation + val simulation = if (simulationResponse == null) { + SimulationResult.FailedToSimulate + } else { + mapToSolanaAssetsDiffs(simulationResponse.accountSummary.accountAssetsDiff) + } + return CheckTransactionResult( + validation = validation, + description = from.result.validation.description, + simulation = simulation, + ) + } + fun mapToDomain(from: TransactionScanResponse): CheckTransactionResult { return CheckTransactionResult( validation = when { @@ -68,7 +91,7 @@ internal object BlockAidMapper { fun mapToSolanaRequest(from: TransactionData): SolanaTransactionScanRequest { return SolanaTransactionScanRequest( - chain = from.chain.lowercase(), + blockchain = from.chain.lowercase(), accountAddress = from.accountAddress, metadata = TransactionMetadata(from.domainUrl), method = from.method, @@ -89,6 +112,49 @@ internal object BlockAidMapper { } } + private fun mapToSolanaAssetsDiffs(assetsDiffs: List): SimulationResult { + val sendInfo = assetsDiffs.mapNotNull { assetDiff -> + val outTransfer = assetDiff.outTransfer ?: return@mapNotNull null + val amount = outTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null + AmountInfo.FungibleTokens( + amount = amount, + token = TokenInfo( + chainId = null, + logoUrl = assetDiff.asset.logoUrl, + symbol = assetDiff.asset.assetSymbol(), + decimals = assetDiff.asset.decimals ?: 0, + ), + ) + } + val receiveInfo = assetsDiffs.mapNotNull { assetDiff -> + val inTransfer = assetDiff.inTransfer ?: return@mapNotNull null + val amount = inTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null + AmountInfo.FungibleTokens( + amount = amount, + token = TokenInfo( + chainId = null, + logoUrl = assetDiff.asset.logoUrl, + symbol = assetDiff.asset.assetSymbol(), + decimals = assetDiff.asset.decimals ?: 0, + ), + ) + } + + return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) { + SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo)) + } else { + SimulationResult.Success(SimulationData.NoWalletChangesDetected) + } + } + + private fun SolanaTransactionAsset.assetSymbol(): String { + return if (type?.lowercase().equals(SOL_ASSET_SYMBOL, ignoreCase = true)) { + symbol ?: SOL_ASSET_SYMBOL + } else { + symbol.orEmpty() + } + } + private fun mapApproveTransaction(exposures: List?): SimulationResult { val amounts = exposures?.flatMap { exposure -> val tokenInfo = TokenInfo( diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt index 66483ed2df..985e8d7e2e 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -24,16 +24,21 @@ internal class DefaultBlockAidRepository( } override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult { - val response = withContext(dispatchers.io) { - when (data.params) { - is TransactionParams.Evm -> { - api.scanJsonRpc(mapper.mapToEvmRequest(data)) - } - is TransactionParams.Solana -> { - api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) - } - } + return when (data.params) { + is TransactionParams.Evm -> scanEvmTransaction(data = data) + is TransactionParams.Solana -> scanSolanaTransaction(data = data) } - return mapper.mapToDomain(response) } + + private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult = + withContext(dispatchers.io) { + val response = api.scanJsonRpc(mapper.mapToEvmRequest(data)) + mapper.mapToDomain(response) + } + + private suspend fun scanSolanaTransaction(data: TransactionData): CheckTransactionResult = + withContext(dispatchers.io) { + val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) + mapper.mapToDomain(response) + } } \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt index d125246bce..54d8331f1f 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -91,7 +92,7 @@ class DefaultBlockAidRepositoryTest { ) val request = mockk() - val response = mockk() + val response = mockk() val expectedResult = mockk() every { mapper.mapToSolanaRequest(data) } returns request diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index eb03530b0a..8bc9a0c492 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -40,6 +40,14 @@ internal class BlockAidVerificationDelegate @Inject constructor( return@flow } emit(Lce.Loading(partialContent = null)) + val methodName = when (method) { + is WcEthMethod -> rawSdkRequest.request.method + is WcSolanaMethod -> method.trimmedPrefixMethodName + is WcMethod.Unsupported -> { + emit(Lce.Content(failedResult)) + return@flow + } + } val params = when (method) { is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params) is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction) @@ -59,7 +67,7 @@ internal class BlockAidVerificationDelegate @Inject constructor( data = TransactionData( chain = chain, accountAddress = accountAddress, - method = rawSdkRequest.request.method, + method = methodName, domainUrl = session.sdkModel.appMetaData.url, params = params, ), diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt index 0042025dfb..d44d98c88f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -2,18 +2,27 @@ package com.tangem.domain.walletconnect.model sealed interface WcSolanaMethod : WcMethod { + val methodName: String + val trimmedPrefixMethodName: String get() = methodName.substringAfter("_") + data class SignMessage( val pubKey: String, val rawMessage: String, val humanMsg: String, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignMessage.raw + } data class SignTransaction( val transaction: String, val address: String?, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignTransaction.raw + } data class SignAllTransaction( val transaction: List, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SendAllTransaction.raw + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt index c51cc78fbb..91ab33bb5b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.utils.converter.Converter -import java.math.BigDecimal import javax.inject.Inject internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : @@ -17,7 +16,7 @@ internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : is AmountInfo.FungibleTokens -> WcEstimatedWalletChangeUM( iconRes = value.iconRes, title = resourceReference(value.titleRes), - description = "${value.sign} ${amountInfo.amount.amountText()} ${amountInfo.token.symbol}", + description = "${value.sign} ${amountInfo.amountText()} ${amountInfo.token.symbol}", tokenIconUrl = amountInfo.token.logoUrl, ) is AmountInfo.NonFungibleTokens -> WcEstimatedWalletChangeUM( @@ -29,7 +28,7 @@ internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : } } - private fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } + private fun AmountInfo.FungibleTokens.amountText() = amount.format { crypto("", token.decimals) } data class Input( val amountInfo: AmountInfo, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 1eda478462..3312c609d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -21,7 +21,7 @@ import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import javax.inject.Inject -internal const val DECIMALS_AMOUNT = 2 +private const val DECIMALS_AMOUNT = 2 @Suppress("CyclomaticComplexMethod", "LongMethod") internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor(