Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-12 12:10:39 +05:00
parent a69cc2b5e6
commit 502d9e3b40
12 changed files with 135 additions and 65 deletions

View file

@ -16,6 +16,7 @@ data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "decimals") val decimals: Int? = null,
)

View file

@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Exposure(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: Asset,
@Json(name = "spenders") val spenders: Map<String, SpenderDetails>,
)

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#0099FF"
android:pathData="M16.539,8.682C16.769,8.338 16.677,7.872 16.332,7.642C15.988,7.412 15.522,7.505 15.292,7.849L10.88,14.452L8.677,11.614C8.423,11.287 7.952,11.228 7.625,11.482C7.297,11.736 7.238,12.207 7.492,12.534L10.333,16.194C10.481,16.384 10.712,16.492 10.953,16.484C11.193,16.475 11.416,16.351 11.55,16.151L16.539,8.682Z" />
<path
android:fillColor="#0099FF"
android:fillType="evenOdd"
android:pathData="M12,1.746C6.337,1.746 1.746,6.337 1.746,12C1.746,17.663 6.337,22.254 12,22.254C17.663,22.254 22.254,17.663 22.254,12C22.254,6.337 17.663,1.746 12,1.746ZM3.246,12C3.246,7.165 7.165,3.246 12,3.246C16.835,3.246 20.754,7.165 20.754,12C20.754,16.835 16.835,20.754 12,20.754C7.165,20.754 3.246,16.835 3.246,12Z" />
</vector>

View file

@ -3,7 +3,7 @@ package com.tangem.data.blockaid
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.transaction.*
import com.domain.blockaid.models.transaction.simultation.AmountInfo
import com.domain.blockaid.models.transaction.simultation.ApprovedAmount
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.domain.blockaid.models.transaction.simultation.TokenInfo
import com.tangem.blockchain.extensions.hexToBigDecimal
@ -101,13 +101,8 @@ internal object BlockAidMapper {
private fun mapSimulationSuccessResult(from: AccountSummaryResponse): SimulationResult {
return when {
!from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction(
from.assetsDiffs,
)
!from.exposures.isNullOrEmpty() -> mapApproveTransaction(
from.exposures,
)
!from.traces.isNullOrEmpty() -> mapNftSendReceiveTransaction(from.traces)
!from.exposures.isNullOrEmpty() -> mapApproveTransaction(from.exposures)
!from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction(from.assetsDiffs)
else -> SimulationResult.Success(data = SimulationData.NoWalletChangesDetected)
}
}
@ -156,23 +151,11 @@ internal object BlockAidMapper {
}
private fun mapApproveTransaction(exposures: List<Exposure>?): SimulationResult {
val amounts = exposures?.flatMap { exposure ->
val tokenInfo = TokenInfo(
chainId = exposure.asset.chainId,
logoUrl = exposure.asset.logoUrl,
symbol = exposure.asset.symbol ?: "",
decimals = exposure.asset.decimals ?: 0,
)
exposure.spenders.flatMap { (_, spender) ->
val isUnlimited = spender.isApprovedForAll == true
val approval = spender.approval?.hexToBigDecimal()
spender.exposure.map { detail ->
ApprovedAmount(
approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(),
isUnlimited = isUnlimited,
tokenInfo = tokenInfo,
)
}
val amounts: List<ApproveInfo>? = exposures?.flatMap { exposure ->
if (exposure.assetType.isNFT()) {
listOf(mapApproveNftTransaction(exposure))
} else {
mapTransaction(exposure)
}
}
return if (!amounts.isNullOrEmpty()) {
@ -182,6 +165,34 @@ internal object BlockAidMapper {
}
}
private fun mapTransaction(exposure: Exposure): List<ApproveInfo.Amount> {
val tokenInfo = TokenInfo(
chainId = exposure.asset.chainId,
logoUrl = exposure.asset.logoUrl,
symbol = exposure.asset.symbol ?: "",
decimals = exposure.asset.decimals ?: 0,
)
return exposure.spenders.flatMap { (_, spender) ->
val isUnlimited = spender.isApprovedForAll == true
val approval = spender.approval?.hexToBigDecimal()
spender.exposure.map { detail ->
ApproveInfo.Amount(
approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(),
isUnlimited = isUnlimited,
tokenInfo = tokenInfo,
)
}
}
}
private fun mapApproveNftTransaction(exposure: Exposure): ApproveInfo.NonFungibleToken {
return ApproveInfo.NonFungibleToken(
name = exposure.asset.name.orEmpty(),
logoUrl = exposure.spenders.values.firstOrNull()?.exposure?.firstOrNull()?.logoUrl
?: exposure.asset.logoUrl,
)
}
private fun mapSendReceiveTransaction(assetDiffs: List<AssetDiff>?): SimulationResult {
val sendInfo = arrayListOf<AmountInfo>()
val receiveInfo = arrayListOf<AmountInfo>()
@ -194,13 +205,31 @@ internal object BlockAidMapper {
decimals = diff.asset.decimals ?: 0,
)
diff.outTransfer.orEmpty().forEach { transfer ->
transfer.value?.toBigDecimalOrNull()?.let { amount ->
sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
if (diff.assetType.isNFT()) {
sendInfo.add(
AmountInfo.NonFungibleTokens(
name = diff.asset.name.orEmpty(),
logoUrl = token.logoUrl,
),
)
} else {
transfer.value?.toBigDecimalOrNull()?.let { amount ->
sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
}
}
}
diff.inTransfer.orEmpty().forEach { transfer ->
transfer.value?.toBigDecimalOrNull()?.let { amount ->
receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
if (diff.assetType.isNFT()) {
receiveInfo.add(
AmountInfo.NonFungibleTokens(
name = diff.asset.name.orEmpty(),
logoUrl = token.logoUrl,
),
)
} else {
transfer.value?.toBigDecimalOrNull()?.let { amount ->
receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
}
}
}
}
@ -212,17 +241,7 @@ internal object BlockAidMapper {
}
}
private fun mapNftSendReceiveTransaction(traces: List<Trace>?): SimulationResult {
val sendInfo = traces?.mapNotNull {
it.exposed?.let { exposed ->
AmountInfo.NonFungibleTokens(name = "${it.asset.name} #${exposed.tokenId}", logoUrl = exposed.logoUrl)
}
}
return if (!sendInfo.isNullOrEmpty()) {
SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = listOf()))
} else {
SimulationResult.Success(SimulationData.NoWalletChangesDetected)
}
private fun String.isNFT(): Boolean {
return this.lowercase() == "erc721" || this.lowercase() == "erc1155" || this.lowercase() == "nft"
}
}

View file

@ -4,6 +4,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.ValidationResult
import com.domain.blockaid.models.transaction.simultation.AmountInfo
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.google.common.truth.Truth
import com.tangem.datasource.api.common.blockaid.models.response.*
@ -44,6 +45,7 @@ class BlockAidMapperTest {
val exposure = Exposure(
asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE", decimals = 8),
spenders = mapOf("spender" to spenderDetails),
assetType = "native",
)
val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""),
@ -65,9 +67,10 @@ class BlockAidMapperTest {
val approve = simulation?.data as? SimulationData.Approve
Truth.assertThat(approve).isNotNull()
Truth.assertThat(approve?.approvedAmounts?.size).isEqualTo(1)
Truth.assertThat(approve?.approvedAmounts?.first()?.approvedAmount).isEqualTo(BigDecimal("1000.0"))
Truth.assertThat(approve?.approvedAmounts?.first()?.isUnlimited).isTrue()
Truth.assertThat(approve?.items?.size).isEqualTo(1)
Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.approvedAmount)
.isEqualTo(BigDecimal("1000.0"))
Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.isUnlimited).isTrue()
}
@Test

View file

@ -2,7 +2,7 @@ package com.tangem.data.walletconnect.network.ethereum
import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.simultation.ApprovedAmount
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
@ -68,13 +68,15 @@ internal class WcEthTxHelper @Inject constructor(
)
}
fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApprovedAmount? {
fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApproveInfo.Amount? {
val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId
val isApprovalWcMethod = txData?.startsWith(approvalMethodId)
if (isApprovalWcMethod != true) return null
val simulation = result.simulation as? SimulationResult.Success
?: return null
val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts
val approves = (simulation.data as? SimulationData.Approve)
?.items
?.filterIsInstance<ApproveInfo.Amount>()
?: return null
if (approves.isEmpty()) return null
val amount = approves.first()

View file

@ -0,0 +1,13 @@
package com.domain.blockaid.models.transaction.simultation
import java.math.BigDecimal
sealed class ApproveInfo {
data class Amount(
val approvedAmount: BigDecimal,
val isUnlimited: Boolean,
val tokenInfo: TokenInfo,
) : ApproveInfo()
data class NonFungibleToken(val name: String, val logoUrl: String?) : ApproveInfo()
}

View file

@ -1,9 +0,0 @@
package com.domain.blockaid.models.transaction.simultation
import java.math.BigDecimal
data class ApprovedAmount(
val approvedAmount: BigDecimal,
val isUnlimited: Boolean,
val tokenInfo: TokenInfo,
)

View file

@ -16,9 +16,7 @@ sealed class SimulationData {
/**
* Represents an approve operation with the specified amount (can be multiple amounts for NFT)
*/
data class Approve(
val approvedAmounts: List<ApprovedAmount>,
) : SimulationData()
data class Approve(val items: List<ApproveInfo>) : SimulationData()
/**
* Simulation was successfully performed and no changes detected

View file

@ -132,8 +132,7 @@ private fun EstimatedWalletChangesPreviewMoreThanFour(
}
}
private class EstimatedWalletChangesPreviewProviderTwoItems :
PreviewParameterProvider<WcEstimatedWalletChangesUM> {
private class EstimatedWalletChangesPreviewProviderTwoItems : PreviewParameterProvider<WcEstimatedWalletChangesUM> {
override val values = sequenceOf(
WcEstimatedWalletChangesUM(
items = persistentListOf(
@ -149,6 +148,13 @@ private class EstimatedWalletChangesPreviewProviderTwoItems :
description = "+ 1,131.46 MATIC",
tokenIconUrl = "https://tangem.com",
),
WcEstimatedWalletChangeUM(
iconRes = R.drawable.img_approvale_new_24,
title = resourceReference(R.string.common_approve),
description = "10 Collection",
tokenIconUrl = "https://cdn.blockaid.io/nft/0x09851531816f78cF4841f1DeF22fbaB78aDD02c5/29805/" +
"polygon?r=ed117da0-6065-4ff8-ba81-cb7395e1ec3d",
),
),
),
)

View file

@ -4,6 +4,7 @@ import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.ValidationResult
import com.domain.blockaid.models.transaction.simultation.AmountInfo
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.crypto
@ -55,7 +56,20 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor(
},
estimatedWalletChanges = (simulation as? SimulationResult.Success)?.data?.let { data ->
when (data) {
is SimulationData.Approve, SimulationData.NoWalletChangesDetected -> null
is SimulationData.NoWalletChangesDetected -> null
is SimulationData.Approve -> {
val nftItems = data.items.mapNotNull { item ->
if (item !is ApproveInfo.NonFungibleToken) return@mapNotNull null
WcEstimatedWalletChangeUM(
iconRes = R.drawable.img_approvale_new_24,
title = TextReference.Res(R.string.common_approve),
description = item.name,
tokenIconUrl = item.logoUrl,
)
}
WcEstimatedWalletChangesUM(nftItems.toImmutableList()).takeIf { nftItems.isNotEmpty() }
}
is SimulationData.SendAndReceive -> {
val items: ImmutableList<WcEstimatedWalletChangeUM> = (
data.send.map {

View file

@ -24,6 +24,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.components.divider.DividerWithPadding
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -122,7 +123,7 @@ internal fun WcSendTransactionModalBottomSheet(
modifier = Modifier.padding(top = 14.dp),
config = state.feeErrorNotification.config,
iconTint = TangemTheme.colors.icon.warning,
containerColor = TangemTheme.colors.button.disabled,
containerColor = TangemTheme.colors.background.action,
)
}
}
@ -301,8 +302,12 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
address = null,
sendEnabled = false,
feeErrorNotification = NotificationUM.Info(
title = stringReference("Insufficient Ethereum"),
subtitle = stringReference("Top up your balance to cover the network fee"),
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = {},
),
),
),
),