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 b1c8894daa..51e5374ae8 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, + @Json(name = "decimals") val decimals: Int, ) @JsonClass(generateAdapter = true) 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 90038d41c4..0ed36e6a25 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 @@ -13,6 +13,7 @@ data class Exposure( data class SpenderDetails( @Json(name = "exposure") val exposure: List, @Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null, + @Json(name = "approval") val approval: String? = null, ) @JsonClass(generateAdapter = true) 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 2a02e346ed..23a8170ec7 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 @@ -2,10 +2,11 @@ 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.SimulationData import com.domain.blockaid.models.transaction.simultation.TokenInfo -import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.tangem.blockchain.extensions.hexToBigDecimal import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.RpcData import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest @@ -78,12 +79,14 @@ internal object BlockAidMapper { chainId = exposure.asset.chainId, logoUrl = exposure.asset.logoUrl, symbol = exposure.asset.symbol, + decimals = exposure.asset.decimals, ) exposure.spenders.flatMap { (_, spender) -> val isUnlimited = spender.isApprovedForAll == true + val approval = spender.approval?.hexToBigDecimal() spender.exposure.mapNotNull { detail -> ApprovedAmount( - approvedAmount = detail.value.toBigDecimalOrNull() ?: return@mapNotNull null, + approvedAmount = approval ?: detail.value.toBigDecimalOrNull() ?: return@mapNotNull null, isUnlimited = isUnlimited, tokenInfo = tokenInfo, ) @@ -106,6 +109,7 @@ internal object BlockAidMapper { chainId = diff.asset.chainId, logoUrl = diff.asset.logoUrl, symbol = diff.asset.symbol, + decimals = diff.asset.decimals, ) diff.outTransfer.orEmpty().forEach { transfer -> transfer.value?.toBigDecimalOrNull()?.let { amount -> 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 5ff9caaee8..6d83fc64db 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 @@ -42,7 +42,7 @@ class BlockAidMapperTest { exposure = listOf(ExposureDetail(value = "1000.0", rawValue = "0x123")), ) val exposure = Exposure( - asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE"), + asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE", decimals = 8), spenders = mapOf("spender" to spenderDetails), ) val response = TransactionScanResponse( @@ -74,7 +74,7 @@ class BlockAidMapperTest { fun `when response benign validation and success simulation then returns send receive result`() { val assetDiff = AssetDiff( assetType = "ERC20", - asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH"), + asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH", decimals = 8), inTransfer = listOf(Transfer(value = "2.0", rawValue = "0x1")), outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")), ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 64c0fb108e..f5ef109750 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -1,6 +1,9 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -11,18 +14,14 @@ import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletconnect.model.WcEthMethod -import com.tangem.domain.walletconnect.usecase.method.WcMutableFee -import com.tangem.domain.walletconnect.usecase.method.WcSignState -import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase +import com.tangem.domain.walletconnect.usecase.method.* import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.* internal class WcEthSendTransactionUseCase @AssistedInject constructor( @Assisted override val context: WcMethodUseCaseContext, @@ -31,18 +30,45 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( override val analytics: AnalyticsEventHandler, private val sendTransaction: SendTransactionUseCase, blockAidDelegate: BlockAidVerificationDelegate, -) : BaseWcSignUseCase(), +) : BaseWcSignUseCase(), WcTransactionUseCase, + WcApproval, WcMutableFee { - override val securityStatus = blockAidDelegate.getSecurityStatus( - network = network, - method = method, - rawSdkRequest = rawSdkRequest, - session = session, - accountAddress = context.accountAddress, + private var approvalAmount: Amount? = null + private var dAppFee = WcEthTxHelper.getDAppFee( + network = context.network, + txParams = method.transaction, ) + override val securityStatus: LceFlow = + blockAidDelegate.getSecurityStatus( + network = network, + method = method, + rawSdkRequest = rawSdkRequest, + session = session, + accountAddress = context.accountAddress, + ).map { lce -> + lce.map { result -> + val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) + ?: return@map BlockAidTransactionCheck.Result.Plain(result) + val tokenInfo = amount.tokenInfo + if (!amount.isUnlimited) { + this@WcEthSendTransactionUseCase.approvalAmount = Amount( + currencySymbol = tokenInfo.symbol, + decimals = tokenInfo.decimals, + value = amount.approvedAmount, + ) + } + BlockAidTransactionCheck.Result.Approval( + result = result, + approval = this, + tokenInfo = tokenInfo, + isMutable = true, + ) + } + } + override suspend fun SignCollector.onSign(state: WcSignState) { val hash = sendTransaction(state.signModel, wallet, network) .onLeft { error -> @@ -54,22 +80,34 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emit(state.toResult(respondResult)) } - override suspend fun FlowCollector.onMiddleAction(signModel: TransactionData, fee: Fee) { + override suspend fun FlowCollector.onMiddleAction( + signModel: TransactionData, + action: WcEthTxAction, + ) { val uncompiled = signModel.requireUncompiled() - val newState = uncompiled.copy(fee = fee) + val newState = when (action) { + is WcEthTxAction.UpdateApprovalAmount -> { + val extras = uncompiled.extras as EthereumTransactionExtras + val callData = ApprovalERC20TokenCallData( + spenderAddress = uncompiled.sourceAddress, + amount = action.amount, + ) + approvalAmount = action.amount + dAppFee = null + uncompiled.copy(extras = extras.copy(callData = callData)) + } + is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee) + } + emit(newState) } override suspend fun dAppFee(): Fee.Ethereum.Legacy? { - val dAppFee = WcEthTxHelper.getDAppFee( - network = context.network, - txParams = method.transaction, - ) return dAppFee } override fun updateFee(fee: Fee) { - middleAction(fee) + middleAction(WcEthTxAction.UpdateFee(fee)) } override fun invoke(): Flow> = flow { @@ -81,6 +119,14 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emitAll(delegate.invoke(transactionData)) } + override suspend fun getAmount(): Amount? { + return approvalAmount + } + + override fun updateAmount(amount: Amount?) { + middleAction(WcEthTxAction.UpdateApprovalAmount(amount)) + } + @AssistedFactory interface Factory { fun create(context: WcMethodUseCaseContext, method: WcEthMethod.SendTransaction): WcEthSendTransactionUseCase diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index 52f4ef6167..e372f35b8a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -1,6 +1,9 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -14,6 +17,8 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.PrepareForSendUseCase import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck +import com.tangem.domain.walletconnect.usecase.method.WcApproval import com.tangem.domain.walletconnect.usecase.method.WcMutableFee import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase @@ -24,6 +29,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map internal class WcEthSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, @@ -32,17 +38,43 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTransaction, blockAidDelegate: BlockAidVerificationDelegate, -) : BaseWcSignUseCase(), +) : BaseWcSignUseCase(), WcTransactionUseCase, + WcApproval, WcMutableFee { + private var approvalAmount: Amount? = null + private var dAppFee = WcEthTxHelper.getDAppFee( + network = context.network, + txParams = method.transaction, + ) + override val securityStatus = blockAidDelegate.getSecurityStatus( network = network, method = method, rawSdkRequest = rawSdkRequest, session = session, accountAddress = context.accountAddress, - ) + ).map { lce -> + lce.map { result -> + val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) + ?: return@map BlockAidTransactionCheck.Result.Plain(result) + val tokenInfo = amount.tokenInfo + if (!amount.isUnlimited) { + approvalAmount = Amount( + currencySymbol = tokenInfo.symbol, + decimals = tokenInfo.decimals, + value = amount.approvedAmount, + ) + } + BlockAidTransactionCheck.Result.Approval( + result = result, + approval = this, + tokenInfo = tokenInfo, + isMutable = true, + ) + } + } override suspend fun SignCollector.onSign(state: WcSignState) { val hash = prepareForSend(state.signModel, wallet, network) @@ -56,14 +88,30 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( emit(state.toResult(respondResult)) } - override suspend fun FlowCollector.onMiddleAction(signModel: TransactionData, fee: Fee) { + override suspend fun FlowCollector.onMiddleAction( + signModel: TransactionData, + action: WcEthTxAction, + ) { val uncompiled = signModel.requireUncompiled() - val newState = uncompiled.copy(fee = fee) + val newState = when (action) { + is WcEthTxAction.UpdateApprovalAmount -> { + val extras = uncompiled.extras as EthereumTransactionExtras + val callData = ApprovalERC20TokenCallData( + spenderAddress = uncompiled.sourceAddress, + amount = action.amount, + ) + approvalAmount = action.amount + dAppFee = null + uncompiled.copy(extras = extras.copy(callData = callData)) + } + is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee) + } + emit(newState) } override fun updateFee(fee: Fee) { - middleAction(fee) + middleAction(WcEthTxAction.UpdateFee(fee)) } override fun invoke(): Flow> = flow { @@ -76,13 +124,17 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( } override suspend fun dAppFee(): Fee.Ethereum.Legacy? { - val dAppFee = WcEthTxHelper.getDAppFee( - network = context.network, - txParams = method.transaction, - ) return dAppFee } + override suspend fun getAmount(): Amount? { + return approvalAmount + } + + override fun updateAmount(amount: Amount?) { + middleAction(WcEthTxAction.UpdateApprovalAmount(amount)) + } + @AssistedFactory interface Factory { fun create(context: WcMethodUseCaseContext, method: WcEthMethod.SignTransaction): WcEthSignTransactionUseCase 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 4090d40647..fdffc8a26d 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 @@ -1,6 +1,11 @@ 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.SimulationData import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.HEX_PREFIX @@ -56,4 +61,23 @@ internal object WcEthTxHelper { ), ) } + + fun getApprovedAmount(txData: String, result: CheckTransactionResult): ApprovedAmount? { + val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId + val isApprovalWcMethod = txData.startsWith(approvalMethodId) + if (!isApprovalWcMethod) return null + val simulation = result.simulation as? SimulationResult.Success + ?: return null + val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts + ?: return null + if (approves.size != 1) return null + val amount = approves.first() + return amount + } +} + +sealed interface WcEthTxAction { + + data class UpdateFee(val fee: Fee) : WcEthTxAction + data class UpdateApprovalAmount(val amount: Amount?) : WcEthTxAction } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt index 1f518d0cdb..de1f1f949e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt @@ -12,12 +12,14 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.PrepareForSendUseCase import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import okio.ByteString.Companion.decodeBase64 internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( @@ -36,7 +38,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( rawSdkRequest = rawSdkRequest, session = session, accountAddress = context.accountAddress, - ) + ).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } } override suspend fun SignCollector.onSign(state: WcSignState) { val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt index 889bcf1d1e..96367d2c08 100644 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt @@ -4,4 +4,5 @@ data class TokenInfo( val chainId: Int?, val logoUrl: String?, val symbol: String, + val decimals: Int, ) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/BlockAidTransactionCheck.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/BlockAidTransactionCheck.kt new file mode 100644 index 0000000000..925877b063 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/BlockAidTransactionCheck.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.walletconnect.usecase.method + +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.simultation.TokenInfo +import com.tangem.domain.core.lce.LceFlow + +interface BlockAidTransactionCheck { + + val securityStatus: LceFlow + + sealed interface Result { + val result: CheckTransactionResult + + data class Plain(override val result: CheckTransactionResult) : Result + + data class Approval( + override val result: CheckTransactionResult, + val approval: WcApproval, + val tokenInfo: TokenInfo, + val isMutable: Boolean, + ) : Result { + + suspend fun approvalAmount() = approval.getAmount() + } + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt index 94696251c0..0acf5cfbd5 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.walletconnect.usecase.method +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.walletconnect.usecase.blockaid.WcBlockAidEligibleTransactionUseCase @@ -16,7 +17,7 @@ import com.tangem.domain.walletconnect.usecase.blockaid.WcBlockAidEligibleTransa */ interface WcTransactionUseCase : WcSignUseCase, - WcBlockAidEligibleTransactionUseCase + BlockAidTransactionCheck /** * Base UseCase for wc methods with list of TransactionData @@ -31,7 +32,7 @@ interface WcListTransactionUseCase : /** * Wc Methods that support an updatable fee * [dAppFee] call to gee fee from dApp - * [updateFee] call when user update the fee + * [updateFee] triggered a new [TransactionData] emit */ interface WcMutableFee { suspend fun dAppFee(): Fee? @@ -39,9 +40,11 @@ interface WcMutableFee { } /** - * Wc Methods that support an updatable allowance - * [updateFee] call when user update the fee + * Wc Approval Method + * If [Amount] is null it means unlimited + * [updateAmount] triggered a new [TransactionData] emit */ -interface WcMutableAllowance { - fun updateAllowance(allowance: Any) // todo wc +interface WcApproval { + suspend fun getAmount(): Amount? + fun updateAmount(amount: Amount?) } \ No newline at end of file