diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt index be70cd4a52..a3c5d878a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt @@ -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, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt index e776056d4c..cfab51b716 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt @@ -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, ) diff --git a/core/ui/src/main/res/drawable/img_approvale_new_24.xml b/core/ui/src/main/res/drawable/img_approvale_new_24.xml new file mode 100644 index 0000000000..bc1f814f54 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_approvale_new_24.xml @@ -0,0 +1,17 @@ + + + + + + + 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 e61f8ddd21..5334f1e066 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 @@ -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?): 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? = 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 { + 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?): SimulationResult { val sendInfo = arrayListOf() val receiveInfo = arrayListOf() @@ -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?): 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" } } \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt index 00dd54e198..3b24f6e0a4 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -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 diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index 057afaa2e9..3526fb2d8c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -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() ?: return null if (approves.isEmpty()) return null val amount = approves.first() diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt new file mode 100644 index 0000000000..2325c38b32 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt @@ -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() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt deleted file mode 100644 index 6fdad58ec2..0000000000 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt +++ /dev/null @@ -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, -) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt index 13c28ebba4..13937ad93e 100644 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt @@ -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, - ) : SimulationData() + data class Approve(val items: List) : SimulationData() /** * Simulation was successfully performed and no changes detected diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt index 25e8b53eba..590b49c95e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -132,8 +132,7 @@ private fun EstimatedWalletChangesPreviewMoreThanFour( } } -private class EstimatedWalletChangesPreviewProviderTwoItems : - PreviewParameterProvider { +private class EstimatedWalletChangesPreviewProviderTwoItems : PreviewParameterProvider { 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", + ), ), ), ) 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 3312c609d3..3ef6afbf4c 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 @@ -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 = ( data.send.map { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index ec1400b0df..8712039925 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -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 = {}, + ), ), ), ),