Updated on 2026-08-14
This commit is contained in:
parent
ae33c00aae
commit
85cff6b0d3
10 changed files with 169 additions and 22 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<String> = listOf("simulation", "validation"),
|
||||
@Json(name = "metadata") val metadata: TransactionMetadata,
|
||||
|
|
|
|||
|
|
@ -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<SolanaTransactionAssetDiff>,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
|
|
@ -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<SolanaTransactionAssetDiff>): 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<Exposure>?): SimulationResult {
|
||||
val amounts = exposures?.flatMap { exposure ->
|
||||
val tokenInfo = TokenInfo(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult =
|
||||
withContext(dispatchers.io) {
|
||||
val response = api.scanJsonRpc(mapper.mapToEvmRequest(data))
|
||||
mapper.mapToDomain(response)
|
||||
}
|
||||
return mapper.mapToDomain(response)
|
||||
|
||||
private suspend fun scanSolanaTransaction(data: TransactionData): CheckTransactionResult =
|
||||
withContext(dispatchers.io) {
|
||||
val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data))
|
||||
mapper.mapToDomain(response)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SolanaTransactionScanRequest>()
|
||||
val response = mockk<TransactionScanResponse>()
|
||||
val response = mockk<SolanaTransactionResponse>()
|
||||
val expectedResult = mockk<CheckTransactionResult>()
|
||||
|
||||
every { mapper.mapToSolanaRequest(data) } returns request
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
) : WcSolanaMethod
|
||||
) : WcSolanaMethod {
|
||||
override val methodName: String = WcSolanaMethodName.SendAllTransaction.raw
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue