Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-27 17:27:18 +07:00
parent 0adc8ffb49
commit 62ddda3e69
11 changed files with 201 additions and 41 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,
@Json(name = "decimals") val decimals: Int,
)
@JsonClass(generateAdapter = true)

View file

@ -13,6 +13,7 @@ data class Exposure(
data class SpenderDetails(
@Json(name = "exposure") val exposure: List<ExposureDetail>,
@Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null,
@Json(name = "approval") val approval: String? = null,
)
@JsonClass(generateAdapter = true)

View file

@ -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 ->

View file

@ -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")),
)

View file

@ -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,17 +30,44 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
override val analytics: AnalyticsEventHandler,
private val sendTransaction: SendTransactionUseCase,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<Fee, TransactionData>(),
) : BaseWcSignUseCase<WcEthTxAction, TransactionData>(),
WcTransactionUseCase,
WcApproval,
WcMutableFee {
override val securityStatus = blockAidDelegate.getSecurityStatus(
private var approvalAmount: Amount? = null
private var dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
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<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = sendTransaction(state.signModel, wallet, network)
@ -54,22 +80,34 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(state.toResult(respondResult))
}
override suspend fun FlowCollector<TransactionData>.onMiddleAction(signModel: TransactionData, fee: Fee) {
override suspend fun FlowCollector<TransactionData>.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<WcSignState<TransactionData>> = 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

View file

@ -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<Fee, TransactionData>(),
) : BaseWcSignUseCase<WcEthTxAction, TransactionData>(),
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<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = prepareForSend(state.signModel, wallet, network)
@ -56,14 +88,30 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
emit(state.toResult(respondResult))
}
override suspend fun FlowCollector<TransactionData>.onMiddleAction(signModel: TransactionData, fee: Fee) {
override suspend fun FlowCollector<TransactionData>.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<WcSignState<TransactionData>> = 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

View file

@ -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
}

View file

@ -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<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)

View file

@ -4,4 +4,5 @@ data class TokenInfo(
val chainId: Int?,
val logoUrl: String?,
val symbol: String,
val decimals: Int,
)

View file

@ -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<Throwable, Result>
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()
}
}
}

View file

@ -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<TransactionData>,
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?)
}