Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-14 18:14:47 +07:00
parent 58d34d0fa7
commit fc4c7ef9ab
10 changed files with 132 additions and 145 deletions

View file

@ -5,7 +5,6 @@ import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.walletconnect.DefaultWalletConnectRepository
import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork
import com.tangem.data.walletconnect.network.solana.WcSolanaNetwork
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate
@ -13,7 +12,7 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
import com.tangem.data.walletconnect.request.DefaultWcRequestService
import com.tangem.data.walletconnect.request.WcMethodHandler
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
import com.tangem.data.walletconnect.respond.DefaultWcRespondService
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager
@ -22,11 +21,9 @@ import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.request.WcRequestService
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -114,37 +111,37 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun defaultWcRequestService(
sessionsManager: WcSessionsManager,
respondService: WcRespondService,
dispatchers: CoroutineDispatcherProvider,
diHelperBox: DiHelperBox,
): DefaultWcRequestService {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcRequestService(
sessionsManager = sessionsManager,
respondService = respondService,
requestAdapters = diHelperBox.handlers,
requestConverters = diHelperBox.handlers,
scope = scope,
)
}
@Provides
@Singleton
fun wcRequestService(default: DefaultWcRequestService): WcRequestService = default
@Provides
@Singleton
fun wcRespondService(): WcRespondService = DefaultWcRespondService()
@Provides
@Singleton
fun wcEthNetwork(@SdkMoshi moshi: Moshi): WcEthNetwork = WcEthNetwork(
fun wcEthNetwork(
@SdkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
sessionsManager: WcSessionsManager,
): WcEthNetwork = WcEthNetwork(
moshi = moshi,
excludedBlockchains = excludedBlockchains,
sessionsManager = sessionsManager,
)
@Provides
@Singleton
fun wcSolanaNetwork(): WcSolanaNetwork = WcSolanaNetwork()
fun wcSolanaNetwork(excludedBlockchains: ExcludedBlockchains): WcSolanaNetwork = WcSolanaNetwork(
excludedBlockchains = excludedBlockchains,
)
@Provides
@Singleton
@ -162,12 +159,10 @@ internal object WalletConnectDataModule {
diHelperBox: DiHelperBox,
getWallets: GetWalletsUseCase,
currenciesRepository: CurrenciesRepository,
excludedBlockchains: ExcludedBlockchains,
): AssociateNetworksDelegate = AssociateNetworksDelegate(
namespaceConverters = diHelperBox.converters,
getWallets = getWallets,
currenciesRepository = currenciesRepository,
excludedBlockchains = excludedBlockchains,
)
@Provides
@ -176,14 +171,14 @@ internal object WalletConnectDataModule {
handlers = setOf(
ethNetwork,
),
converters = buildMap {
ethNetwork.namespaceKey to ethNetwork
solanaNetwork.namespaceKey to solanaNetwork
},
converters = setOf(
ethNetwork,
solanaNetwork,
),
)
internal class DiHelperBox(
val converters: Map<NamespaceKey, WcNamespaceConverter>,
val handlers: Set<WcMethodHandler<WcMethod>>,
val converters: Set<WcNamespaceConverter>,
val handlers: Set<WcRequestToUseCaseConverter>,
)
}

View file

@ -2,36 +2,50 @@ package com.tangem.data.walletconnect.network.ethereum
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.request.WcMethodHandler
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.usecase.WcUseCase
import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.WcEthMethod
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
import com.tangem.domain.wallets.models.UserWallet
internal class WcEthNetwork(
internal class WcEthNetwork constructor(
private val moshi: Moshi,
) : WcMethodHandler<WcEthMethod>, WcUseCasesFlowProvider, WcNamespaceConverter {
private val _useCases: Channel<WcUseCase> = Channel(Channel.BUFFERED)
override val useCases = _useCases.receiveAsFlow()
private val excludedBlockchains: ExcludedBlockchains,
private val sessionsManager: WcSessionsManager,
) : WcRequestToUseCaseConverter, WcNamespaceConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
override fun canHandle(methodName: String): Boolean {
return Name.entries.find { it.raw == methodName } != null
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val methodKey = request.request.method
val name = Name.entries.find { it.raw == methodKey } ?: return null
val method: WcEthMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
return when (method) {
is WcEthMethod.PersonalEthSign -> TODO()
}
}
override fun deserialize(methodName: String, params: String): WcEthMethod? {
val name = Name.entries.find { it.raw == methodName } ?: return null
return when (name) {
Name.Sign -> TODO()
Name.PersonalSign -> TODO()
private fun Name.toMethod(request: WcSdkSessionRequest): WcEthMethod? {
val rawParams = request.request.params
return when (this) {
Name.EthSign,
Name.PersonalSign,
-> moshi.fromJson<List<String>>(rawParams)?.let { list ->
val accountIndex = if (this == Name.EthSign) 0 else 1
val messageIndex = if (this == Name.EthSign) 1 else 0
val account = list.getOrNull(accountIndex) ?: return@let null
val message = list.getOrNull(messageIndex) ?: return@let null
WcEthMethod.PersonalEthSign(account = account, message = message)
}
Name.SignTypeData -> TODO()
Name.SignTypeDataV4 -> TODO()
Name.SignTransaction -> TODO()
@ -39,16 +53,8 @@ internal class WcEthNetwork(
}
}
override fun handle(wcRequest: WcRequest<WcMethod>) {
wcRequest as WcRequest<WcEthMethod>
val useCase = when (wcRequest.method) {
is WcEthMethod.PersonalEthSign -> TODO()
}
_useCases.trySend(useCase)
}
enum class Name(val raw: String) {
Sign("eth_sign"),
EthSign("eth_sign"),
PersonalSign("personal_sign"),
SignTypeData("eth_signTypedData"),
SignTypeDataV4("eth_signTypedData_v4"),
@ -56,13 +62,18 @@ internal class WcEthNetwork(
SendTransaction("eth_sendTransaction"),
}
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override fun toCAIP2(blockchain: Blockchain): CAIP2? {
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(

View file

@ -1,11 +1,17 @@
package com.tangem.data.walletconnect.network.solana
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
internal class WcSolanaNetwork constructor(
private val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
internal class WcSolanaNetwork : WcNamespaceConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toBlockchain(chainId: CAIP2): Blockchain? {
@ -17,7 +23,12 @@ internal class WcSolanaNetwork : WcNamespaceConverter {
}
}
override fun toCAIP2(blockchain: Blockchain): CAIP2? {
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID

View file

@ -1,11 +1,6 @@
package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -16,17 +11,16 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
internal class AssociateNetworksDelegate constructor(
private val namespaceConverters: Map<NamespaceKey, WcNamespaceConverter>,
private val namespaceConverters: Set<WcNamespaceConverter>,
private val getWallets: GetWalletsUseCase,
private val currenciesRepository: CurrenciesRepository,
private val excludedBlockchains: ExcludedBlockchains,
) {
@Throws(WcPairError.UnsupportedNetworks::class)
suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map<UserWallet, ProposalNetwork> {
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
val requiredNamespaces: Set<CAIP2> = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces: Set<CAIP2> = sessionProposal.optionalNamespaces.setOfChainId()
val requiredNamespaces: Set<String> = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces: Set<String> = sessionProposal.optionalNamespaces.setOfChainId()
return userWallets
.associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) }
@ -34,8 +28,8 @@ internal class AssociateNetworksDelegate constructor(
private suspend fun mapNetworksForWallet(
wallet: UserWallet,
requiredNamespaces: Set<CAIP2>,
optionalNamespaces: Set<CAIP2>,
requiredNamespaces: Set<String>,
optionalNamespaces: Set<String>,
): ProposalNetwork {
val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
.filterIsInstance<CryptoCurrency.Coin>()
@ -47,23 +41,10 @@ internal class AssociateNetworksDelegate constructor(
val available = mutableSetOf<Network>()
val notAdded = mutableSetOf<Network>()
fun CAIP2.toBlockchain() = namespaceConverters[NamespaceKey(this.namespace)]?.toBlockchain(this)
fun Blockchain.toNetwork() = getNetwork(
blockchain = this,
extraDerivationPath = null,
scanResponse = wallet.scanResponse,
excludedBlockchains = excludedBlockchains,
)
requiredNamespaces.forEach { chainId ->
val blockchain = chainId.toBlockchain()
if (blockchain == null) {
unknownRequired.add(missingNetworkName(chainId))
return@forEach
}
val wcNetwork = blockchain.toNetwork()
val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
if (wcNetwork == null) {
unknownRequired.add(missingNetworkName(blockchain))
unknownRequired.add(missingNetworkName(chainId))
return@forEach
}
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
@ -74,7 +55,8 @@ internal class AssociateNetworksDelegate constructor(
}
}
optionalNamespaces.forEach { chainId ->
val wcNetwork = chainId.toBlockchain()?.toNetwork() ?: return@forEach
val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
?: return@forEach
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
if (walletNetwork == null) {
available.add(wcNetwork)
@ -92,10 +74,8 @@ internal class AssociateNetworksDelegate constructor(
)
}
private fun Map<String, Wallet.Model.Namespace.Proposal>.setOfChainId(): Set<CAIP2> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }
.mapNotNull { rawChainId -> CAIP2.fromRaw(rawChainId) }.toSet()
private fun Map<String, Wallet.Model.Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
private fun missingNetworkName(chainId: CAIP2): String = chainId.namespace.replaceFirstChar(Char::titlecase)
private fun missingNetworkName(blockchain: Blockchain): String = blockchain.getCoinName()
private fun missingNetworkName(chainId: String): String = chainId.replaceFirstChar(Char::titlecase)
}

View file

@ -1,16 +1,14 @@
package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
internal class CaipNamespaceDelegate constructor(
private val namespaceConverters: Map<NamespaceKey, WcNamespaceConverter>,
private val namespaceConverters: Set<WcNamespaceConverter>,
private val walletManagersFacade: WalletManagersFacade,
) {
@ -19,14 +17,11 @@ internal class CaipNamespaceDelegate constructor(
userWallet: UserWallet,
networks: List<Network>,
): Map<String, Wallet.Model.Namespace.Session> {
val converters = namespaceConverters.values
val result = mutableMapOf<String, Session>()
networks.map { network ->
val blockchain = Blockchain.fromId(network.id.value)
val address = walletManagersFacade.getDefaultAddress(userWallet.walletId, network)
val chainId = converters.firstOrNull { it.toCAIP2(blockchain) != null }?.toCAIP2(blockchain)
val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) }
requireNotNull(chainId)
requireNotNull(address)
CAIP10(chainId = chainId, accountAddress = address)

View file

@ -1,25 +1,22 @@
package com.tangem.data.walletconnect.request
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.request.WcRequestService
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
internal class DefaultWcRequestService(
private val sessionsManager: WcSessionsManager,
private val respondService: WcRespondService,
private val requestAdapters: Set<WcMethodHandler<WcMethod>>,
private val requestConverters: Set<WcRequestToUseCaseConverter>,
private val scope: CoroutineScope,
) : WcRequestService, WcSdkObserver {
) : WcSdkObserver, WcUseCasesFlowProvider {
override val requests: MutableSharedFlow<WcRequest<*>> = MutableSharedFlow()
private val _useCases: Channel<WcMethodUseCase> = Channel(Channel.BUFFERED)
override val useCases = _useCases.receiveAsFlow()
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
@ -27,21 +24,10 @@ internal class DefaultWcRequestService(
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
val sr = WcSdkSessionRequestConverter.convert(sessionRequest)
val method = sr.request.method
val params = sr.request.params
scope.launch {
val session = sessionsManager.findSessionByTopic(sr.topic)
val handler: WcMethodHandler<WcMethod>? = requestAdapters.firstOrNull { it.canHandle(method) }
val deserialized: WcMethod? = handler?.deserialize(method, params)
if (handler == null || deserialized == null || session == null) {
respondService.rejectRequest(sr, "UnsupportedMethod") // todo(wc) use our domain error
return@launch
}
val wcRequest = WcRequest(sr, session, deserialized)
handler.handle(wcRequest)
requests.emit(wcRequest)
val useCase = requestConverters.firstNotNullOfOrNull { it.toUseCase(sr) }
?: return@launch // todo(wc) handle unsupported
_useCases.trySend(useCase)
}
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.data.walletconnect.request
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
interface WcMethodHandler<out M : WcMethod> {
fun canHandle(methodName: String): Boolean
fun deserialize(methodName: String, params: String): M?
fun handle(wcRequest: WcRequest<WcMethod>)
companion object {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> fromJson(params: String, moshi: Moshi): T? =
runCatching { moshi.adapter<T>().fromJsonValue(params) }.getOrNull()
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.walletconnect.request
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
interface WcRequestToUseCaseConverter {
suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase?
companion object {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> Moshi.fromJson(params: String): T? =
runCatching { this.adapter<T>().fromJson(params) }.getOrNull()
}
}

View file

@ -1,12 +1,31 @@
package com.tangem.data.walletconnect.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
internal interface WcNamespaceConverter {
val namespaceKey: NamespaceKey
fun toBlockchain(chainId: CAIP2): Blockchain?
fun toCAIP2(blockchain: Blockchain): CAIP2?
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?
fun toNetwork(chainId: String, wallet: UserWallet, excludedBlockchains: ExcludedBlockchains): Network? {
val blockchain = toBlockchain(chainId) ?: return null
return getNetwork(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = wallet.scanResponse,
excludedBlockchains = excludedBlockchains,
)
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.domain.walletconnect.request
import com.tangem.domain.walletconnect.model.WcRequest
import kotlinx.coroutines.flow.Flow
interface WcRequestService {
val requests: Flow<WcRequest<*>>
}