Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-11 16:24:00 +07:00
parent 9e38389156
commit 7493e56a52
22 changed files with 216 additions and 74 deletions

View file

@ -72,7 +72,9 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun defaultWcPairUseCase(): WcPairService = DefaultWcPairService()
fun defaultWcPairUseCase(sessionsManager: DefaultWcSessionsManager): WcPairService = DefaultWcPairService(
sessionsManager,
)
@Provides
@Singleton
@ -90,6 +92,7 @@ internal object WalletConnectDataModule {
legacyStore: WalletConnectSessionsRepository,
getWallets: GetWalletsUseCase,
associateNetworks: AssociateNetworksDelegate,
analytics: AnalyticsEventHandler,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcSessionsManager(
@ -98,6 +101,7 @@ internal object WalletConnectDataModule {
legacyStore = legacyStore,
getWallets = getWallets,
associateNetworks = associateNetworks,
analytics = analytics,
scope = scope,
)
}

View file

@ -3,7 +3,6 @@ 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 as BlockchainAmount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
@ -25,13 +24,16 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.*
import com.tangem.blockchain.common.Amount as BlockchainAmount
@Suppress("LongParameterList")
internal class WcEthSendTransactionUseCase @AssistedInject constructor(
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SendTransaction,
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val sendTransaction: SendTransactionUseCase,
private val ethTxHelper: WcEthTxHelper,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<WcEthTxAction, TransactionData>(),
WcTransactionUseCase,
@ -39,10 +41,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
private var dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
private var dAppFee: Fee? = null
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
blockAidDelegate.getSecurityStatus(
@ -53,7 +52,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
accountAddress = context.accountAddress,
).map { lce ->
lce.map { result ->
val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result)
val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result)
?: return@map BlockAidTransactionCheck.Result.Plain(result)
val tokenInfo = amount.tokenInfo
this@WcEthSendTransactionUseCase.approvalAmount = WcApprovedAmount(
@ -112,7 +111,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(newState)
}
override fun dAppFee(): Fee.Ethereum.Legacy? {
override fun dAppFee(): Fee? {
return dAppFee
}
@ -121,8 +120,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee(),
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
network = context.network,
txParams = method.transaction,
) ?: return@flow

View file

@ -3,7 +3,6 @@ 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 as BlockchainAmount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
@ -24,16 +23,15 @@ 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.map
import kotlinx.coroutines.flow.*
import com.tangem.blockchain.common.Amount as BlockchainAmount
@Suppress("LongParameterList")
internal class WcEthSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
private val ethTxHelper: WcEthTxHelper,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SignTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
@ -43,10 +41,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
private var dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
private var dAppFee: Fee? = null
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
@ -56,7 +51,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
accountAddress = context.accountAddress,
).map { lce ->
lce.map { result ->
val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result)
val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result)
?: return@map BlockAidTransactionCheck.Result.Plain(result)
val tokenInfo = amount.tokenInfo
if (!amount.isUnlimited) {
@ -120,15 +115,16 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee(),
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
network = context.network,
txParams = method.transaction,
) ?: return@flow
emitAll(delegate.invoke(transactionData))
}
override fun dAppFee(): Fee.Ethereum.Legacy? {
override fun dAppFee(): Fee? {
return dAppFee
}

View file

@ -7,40 +7,43 @@ 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
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.blockchain.extensions.hexToBigInteger
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.extensions.hexToBytes
import com.tangem.data.common.currency.getCoinId
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase
import com.tangem.domain.walletconnect.model.WcApprovedAmount
import com.tangem.domain.walletconnect.model.WcEthTransactionParams
import java.math.BigDecimal
import com.tangem.domain.wallets.models.UserWallet
import javax.inject.Inject
internal object WcEthTxHelper {
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
internal class WcEthTxHelper @Inject constructor(
private val getSingleCryptoCurrency: GetSingleCryptoCurrencyStatusUseCase,
private val ethSpecificFee: GetEthSpecificFeeUseCase,
) {
fun getDAppFee(network: Network, txParams: WcEthTransactionParams): Fee.Ethereum.Legacy? {
val gasLimit = txParams.gas?.hexToBigDecimal() ?: return null
val gasPrice = txParams.gasPrice?.hexToBigDecimal() ?: return null
val blockchain = network.toBlockchain()
var feeDecimal = (gasLimit * gasPrice)
.movePointLeft(blockchain.decimals())
if (blockchain == Blockchain.Mantle) {
feeDecimal = feeDecimal.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
}
val feeAmount = Amount(feeDecimal, blockchain)
return Fee.Ethereum.Legacy(feeAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger())
suspend fun getDAppFee(txParams: WcEthTransactionParams, userWallet: UserWallet, network: Network): Fee? {
val gasLimit = txParams.gas?.hexToBigInteger() ?: return null
val gasPrice = txParams.gasPrice?.hexToBigInteger()
val coinId = getCoinId(network, network.toBlockchain().toCoinId())
val currency = getSingleCryptoCurrency.invokeMultiWalletSync(userWallet.walletId, coinId)
.map { it.currency }
.getOrNull() ?: return null
return ethSpecificFee(userWallet, currency, gasLimit, gasPrice)
.map { it.minimum }
.getOrNull()
}
fun createTransactionData(
dAppFee: Fee.Ethereum.Legacy?,
dAppFee: Fee?,
network: Network,
txParams: WcEthTransactionParams,
): TransactionData.Uncompiled? {

View file

@ -1,17 +1,45 @@
package com.tangem.data.walletconnect.pair
import androidx.core.net.toUri
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject
class DefaultWcPairService @Inject constructor() : WcPairService {
class DefaultWcPairService @Inject constructor(
private val sessionsManager: WcSessionsManager,
) : WcPairService {
private val _pairFlow: Channel<WcPairRequest> = Channel(Channel.BUFFERED)
override val pairFlow: Flow<WcPairRequest> = _pairFlow.receiveAsFlow()
override val pairFlow: Flow<WcPairRequest> = _pairFlow
.receiveAsFlow()
.filter(::filterDeeplinkRequestWithSessionTopic)
override fun pair(request: WcPairRequest) {
_pairFlow.trySend(request)
}
// some dApp sends deeplink with session request
// we filter session exist, but start dApp pair flow if unexist
private suspend fun filterDeeplinkRequestWithSessionTopic(request: WcPairRequest): Boolean {
when (request.source) {
WcPairRequest.Source.QR,
WcPairRequest.Source.CLIPBOARD,
WcPairRequest.Source.ETC,
-> return true
WcPairRequest.Source.DEEPLINK -> Unit
}
val isExistSession = existSessionTopic(request.uri).getOrNull() ?: false
return !isExistSession
}
private suspend fun existSessionTopic(uri: String) = runCatching {
val sessionTopic = uri.toUri().getQueryParameter("sessionTopic") ?: return@runCatching false
val isExistSession = sessionsManager.findSessionByTopic(sessionTopic) != null
return@runCatching isExistSession
}
}

View file

@ -8,10 +8,10 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.reown.walletkit.client.Wallet
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.repository.WcSessionsManager
@ -37,6 +37,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
private val onCallTerminalAction = Channel<TerminalAction>()
@Suppress("LongMethod")
override operator fun invoke(): Flow<WcPairState> {
val (uri: String, source: WcPairRequest.Source) = pairRequest
return flow {
@ -47,6 +48,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
val sdkSessionProposal = sdkDelegate.pair(uri)
.onLeft {
Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest")
analytics.send(WcAnalyticEvents.PairFailed)
emit(WcPairState.Error(it))
}
.getOrNull() ?: return@flow
@ -76,7 +78,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}
// finish flow if rejected above
if (sessionForApprove == null) {
analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession))
analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession.dAppMetaData))
sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey)
return@flow
}
@ -155,7 +157,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
analytics.send(
WcAnalyticEvents.PairRequested(
network = requestedNetworks,
verificationInfo.name,
verificationInfo,
),
)
val appMetaData = WcAppMetaData(

View file

@ -6,11 +6,13 @@ import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionDTO
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
@ -24,12 +26,14 @@ import kotlinx.coroutines.flow.*
import timber.log.Timber
import kotlin.coroutines.resume
@Suppress("LongParameterList")
internal class DefaultWcSessionsManager(
private val store: WalletConnectStore,
private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val associateNetworks: AssociateNetworksDelegate,
private val analytics: AnalyticsEventHandler,
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
@ -68,6 +72,7 @@ internal class DefaultWcSessionsManager(
val topic = session.sdkModel.topic
val sdkCall = sdkDisconnectSession(topic)
.onRight { onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = "")) }
analytics.send(WcAnalyticEvents.SessionDisconnected(session.sdkModel.appMetaData))
return sdkCall
}