Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-29 13:43:28 +07:00
parent 9c68c9ff00
commit 4f499f41c7
17 changed files with 159 additions and 179 deletions

View file

@ -7,7 +7,6 @@ import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
@ -15,7 +14,6 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -156,16 +154,6 @@ internal class WcEthNetwork(
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = network.toBlockchain()
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
}
}
internal class Factories @Inject constructor(

View file

@ -41,6 +41,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
// in case of change TransactionExtras
// change approvalAmount for example
private var isIgnoreDAppFee: Boolean = false
private var dAppFee: Fee? = null
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
@ -102,7 +105,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
},
)
approvalAmount = action.amount
dAppFee = null
isIgnoreDAppFee = true
uncompiled.copy(extras = extras.copy(callData = callData))
}
is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
@ -111,8 +114,11 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(newState)
}
override fun dAppFee(): Fee? {
return dAppFee
override suspend fun dAppFee(): Fee? {
if (isIgnoreDAppFee) return null
if (dAppFee != null) return dAppFee
return ethTxHelper.getDAppFee(method.transaction, wallet, network)
.also { dAppFee = it }
}
override fun updateFee(fee: Fee) {
@ -120,9 +126,8 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
dAppFee = dAppFee(),
network = context.network,
txParams = method.transaction,
) ?: return@flow

View file

@ -41,6 +41,9 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
// in case of change TransactionExtras
// change approvalAmount for example
private var isIgnoreDAppFee: Boolean = false
private var dAppFee: Fee? = null
override val securityStatus = blockAidDelegate.getSecurityStatus(
@ -101,7 +104,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
},
)
approvalAmount = action.amount
dAppFee = null
isIgnoreDAppFee = true
uncompiled.copy(extras = extras.copy(callData = callData))
}
is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
@ -115,17 +118,19 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
dAppFee = dAppFee(),
network = context.network,
txParams = method.transaction,
) ?: return@flow
emitAll(delegate.invoke(transactionData))
}
override fun dAppFee(): Fee? {
return dAppFee
override suspend fun dAppFee(): Fee? {
if (isIgnoreDAppFee) return null
if (dAppFee != null) return dAppFee
return ethTxHelper.getDAppFee(method.transaction, wallet, network)
.also { dAppFee = it }
}
override fun getAmount(): WcApprovedAmount? {

View file

@ -8,7 +8,6 @@ import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.toHexString
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
@ -17,7 +16,6 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
@ -88,27 +86,14 @@ internal class WcSolanaNetwork(
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toBlockchain(chainId: CAIP2): Blockchain? {
val isMainNet = MAINNET_CHAIN_ID.any { it.lowercase() == chainId.reference.lowercase() }
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
return when {
isMainNet -> Blockchain.Solana
chainId.reference.lowercase() == TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = network.toBlockchain()
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID
else -> null
}
chainId ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId,
)
}
}
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): Either<Throwable, WcSolanaMethod?> {
@ -140,7 +125,7 @@ internal class WcSolanaNetwork(
)
companion object {
private const val MAINNET_CHAIN_ID = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
private val MAINNET_CHAIN_ID = listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")
private const val TESTNET_CHAIN_ID = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"
}
}

View file

@ -110,8 +110,10 @@ internal class AssociateNetworksDelegate(
.distinctBy { it.rawId }
}
private fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
private fun missingNetworkName(chainId: String): String = chainId.replaceFirstChar(Char::titlecase)
companion object {
internal fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate.Companion.setOfChainId
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
@ -22,6 +24,26 @@ internal class CaipNamespaceDelegate(
val userWallet = sessionForApprove.wallet
val result = mutableMapOf<String, Session>()
val requiredNamespaces = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces = sessionProposal.optionalNamespaces.setOfChainId()
val allWcNetworks = (requiredNamespaces + optionalNamespaces)
.mapNotNull { chainId ->
val network = namespaceConverters
.firstNotNullOfOrNull { it.toNetwork(chainId, userWallet) }
?: return@mapNotNull null
val caip2 = CAIP2.fromRaw(chainId) ?: return@mapNotNull null
network to caip2
}
suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? {
val address = walletManagersFacade.getDefaultAddress(userWalletId, network)
val chainId = allWcNetworks
.find { (wcNetwork, _) -> network.rawId == wcNetwork.rawId }
?.second
if (chainId == null || address == null) return null
return CAIP10(chainId = chainId, accountAddress = address)
}
wcNetworksConverter.convertNetworksForApprove(sessionForApprove)
.mapNotNull { createCAIP10(userWallet.walletId, it) }
.forEach { account ->
@ -52,13 +74,6 @@ internal class CaipNamespaceDelegate(
}
}
private suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? {
val address = walletManagersFacade.getDefaultAddress(userWalletId, network)
val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) }
if (chainId == null || address == null) return null
return CAIP10(chainId = chainId, accountAddress = address)
}
private data class Session(
val chains: MutableSet<String> = mutableSetOf(),
val accounts: MutableSet<String> = mutableSetOf(),

View file

@ -115,6 +115,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}.onCompletion {
if (it != null) {
Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest")
emit(WcPairState.Error(WcPairError.Unknown(it.message.orEmpty())))
} else {
Timber.tag(WC_TAG).i("Completed successfully $pairRequest")
}
@ -134,7 +135,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
private suspend fun walletKitApproveSession(
sessionForApprove: WcSessionApprove,
sdkSessionProposal: Wallet.Model.SessionProposal,
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> {
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> = try {
val namespaces = caipNamespaceDelegate.associate(
sdkSessionProposal,
sessionForApprove,
@ -143,7 +144,10 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
proposerPublicKey = sdkSessionProposal.proposerPublicKey,
namespaces = namespaces,
)
return sdkDelegate.approve(sessionApprove)
sdkDelegate.approve(sessionApprove)
} catch (e: Throwable) {
Timber.tag(WC_TAG).e(e, "Failed to sdk approve session $pairRequest")
WcPairError.ApprovalFailed(e.message.orEmpty()).left()
}
private suspend fun buildProposalState(

View file

@ -150,7 +150,7 @@ internal class WcPairSdkDelegate : WcSdkObserver {
private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left()
companion object {
private const val CALLBACK_TIMEOUT = 30
private const val CALLBACK_TIMEOUT = 60
// com.reown.android.pairing.engine.domain.PairingEngine.pair
private val pairingExpiredMessages = listOf(
"Pairing URI expired",

View file

@ -52,7 +52,7 @@ internal class DefaultWcSessionsManager(
if (someMigrated) return@transform // ignore emit, wait next one
}
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
val someRemove = removeUnknownSessions(inStore, associatedSessions)
val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)
if (someRemove) return@transform // ignore emit, wait next one
emit(associatedSessions.groupBy { it.wallet })
}
@ -79,7 +79,7 @@ internal class DefaultWcSessionsManager(
override suspend fun removeSession(session: WcSession): Either<Throwable, Unit> {
val topic = session.sdkModel.topic
val sdkCall = sdkDisconnectSession(topic)
.onRight { onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = "")) }
onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = ""))
analytics.send(WcAnalyticEvents.SessionDisconnected(session.sdkModel.appMetaData))
return sdkCall
}
@ -142,10 +142,17 @@ internal class DefaultWcSessionsManager(
return wcSessions
}
private suspend fun removeUnknownSessions(storeSessions: Set<WcSessionDTO>, wcSessions: List<WcSession>): Boolean {
private suspend fun removeUnknownSessions(
storeSessions: Set<WcSessionDTO>,
inSdkSessions: List<Wallet.Model.Session>,
wcSessions: List<WcSession>,
): Boolean {
val unknownStoredSessions = storeSessions
.filterNot { dto -> wcSessions.any { it.sdkModel.topic == dto.topic } }
val haveSomeUnknown = unknownStoredSessions.isNotEmpty()
val unknownSdkSessions = inSdkSessions
.filterNot { sdkSession -> wcSessions.any { it.sdkModel.topic == sdkSession.topic } }
val haveSomeUnknownSdkSessions = unknownSdkSessions.isNotEmpty()
if (haveSomeUnknown) {
Timber.tag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions")
@ -164,7 +171,10 @@ internal class DefaultWcSessionsManager(
store.removeSessions(emptyNetworksDto)
}
if (haveEmptySessions) {
emptyNetworkSessions.map { scope.launch { sdkDisconnectSession(it.sdkModel.topic) } }
emptyNetworkSessions.forEach { scope.launch { sdkDisconnectSession(it.sdkModel.topic) } }
}
if (haveSomeUnknownSdkSessions) {
unknownSdkSessions.forEach { scope.launch { sdkDisconnectSession(it.topic) } }
}
return haveSomeUnknown || haveEmptyDto
}

View file

@ -16,7 +16,6 @@ internal interface WcNamespaceConverter {
fun toBlockchain(chainId: CAIP2): Blockchain?
fun toBlockchain(chainId: String): Blockchain? = toCAIP2(chainId)?.let { caip2 -> toBlockchain(caip2) }
fun toCAIP2(network: Network): CAIP2?
fun toCAIP2(chainId: String): CAIP2? = CAIP2.fromRaw(chainId)
fun toNetwork(chainId: String, wallet: UserWallet): Network? {