Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-28 00:23:50 +07:00
parent 80d4eeaed9
commit e2bdc12ed8
24 changed files with 696 additions and 169 deletions

View file

@ -14,7 +14,14 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.walletConnect)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.data.common)
/* Project - Data */
implementation(projects.core.datasource)
@ -26,6 +33,9 @@ dependencies {
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/* Tangem libraries */
implementation(tangemDeps.blockchain)
/* Reown - WalletConnect */
implementation(deps.reownCore) {
exclude(group = "app.cash.sqldelight", module = "android-driver")

View file

@ -1,13 +1,40 @@
package com.tangem.data.walletconnect.di
import android.app.Application
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
import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.request.DefaultWcRequestService
import com.tangem.data.walletconnect.request.WcMethodHandler
import com.tangem.data.walletconnect.respond.DefaultWcRespondService
import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.WcMethod
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.respond.WcRespondService
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -22,4 +49,119 @@ internal object WalletConnectDataModule {
): WalletConnectRepository {
return DefaultWalletConnectRepository(userWalletsStore, dispatchers)
}
@Provides
@Singleton
fun wcInitializeUseCase(
application: Application,
sessionsManager: DefaultWcSessionsManager,
networkService: DefaultWcRequestService,
wcPairFlow: DefaultWcPairUseCase,
): WcInitializeUseCase = DefaultWcInitializeUseCase(
application = application,
sessionsManager = sessionsManager,
networkService = networkService,
wcPairFlow = wcPairFlow,
)
@Provides
@Singleton
fun defaultWcPairUseCase(
sessionsManager: WcSessionsManager,
associateNetworksDelegate: AssociateNetworksDelegate,
caipNamespaceDelegate: CaipNamespaceDelegate,
): DefaultWcPairUseCase = DefaultWcPairUseCase(
sessionsManager = sessionsManager,
associateNetworksDelegate = associateNetworksDelegate,
caipNamespaceDelegate = caipNamespaceDelegate,
)
@Provides
@Singleton
fun wcPairUseCase(default: DefaultWcPairUseCase): WcPairUseCase = default
@Provides
@Singleton
fun defaultWcSessionsManager(): DefaultWcSessionsManager = DefaultWcSessionsManager()
@Provides
@Singleton
fun wcSessionsManager(default: DefaultWcSessionsManager): WcSessionsManager = default
@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,
scope = scope,
)
}
@Provides
@Singleton
fun wcRequestService(default: DefaultWcRequestService): WcRequestService = default
@Provides
@Singleton
fun wcRespondService(): WcRespondService = DefaultWcRespondService()
@Provides
@Singleton
fun wcEthNetwork(@SdkMoshi moshi: Moshi, respondService: WcRespondService): WcEthNetwork = WcEthNetwork(
moshi = moshi,
respondService = respondService,
)
@Provides
@Singleton
fun wcSolanaNetwork(): WcSolanaNetwork = WcSolanaNetwork()
@Provides
@Singleton
fun caipNamespaceDelegate(
diHelperBox: DiHelperBox,
walletManagersFacade: WalletManagersFacade,
): CaipNamespaceDelegate = CaipNamespaceDelegate(
namespaceConverters = diHelperBox.converters,
walletManagersFacade = walletManagersFacade,
)
@Provides
@Singleton
fun associateNetworksDelegate(
diHelperBox: DiHelperBox,
getWallets: GetWalletsUseCase,
currenciesRepository: CurrenciesRepository,
excludedBlockchains: ExcludedBlockchains,
): AssociateNetworksDelegate = AssociateNetworksDelegate(
namespaceConverters = diHelperBox.converters,
getWallets = getWallets,
currenciesRepository = currenciesRepository,
excludedBlockchains = excludedBlockchains,
)
@Provides
@Singleton
fun diHelperBox(ethNetwork: WcEthNetwork, solanaNetwork: WcSolanaNetwork) = DiHelperBox(
handlers = setOf(
ethNetwork,
),
converters = buildMap {
ethNetwork.namespaceKey to ethNetwork
solanaNetwork.namespaceKey to solanaNetwork
},
)
internal class DiHelperBox(
val converters: Map<NamespaceKey, WcNamespaceConverter>,
val handlers: Set<WcMethodHandler<WcMethod>>,
)
}

View file

@ -6,9 +6,9 @@ import com.reown.android.CoreClient
import com.reown.android.relay.ConnectionType
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.DefaultWcSessionsManager
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.request.DefaultWcRequestService
import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import timber.log.Timber

View file

@ -0,0 +1,40 @@
package com.tangem.data.walletconnect.model
import com.tangem.data.walletconnect.model.CAIP2.Companion.CAIP_SEPARATOR
/**
* CAIP-10 defines a way to identify an account in any blockchain specified by [CAIP2] blockchain id.
*
* Syntax
* account_id: chain_id + ":" + account_address
* chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} @see[CAIP2]
* account_address: [-.%a-zA-Z0-9]{1,128}
*
* Examples
* # Ethereum mainnet
* eip155:1:0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb
*
* # Bitcoin mainnet
* bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6
*
* For more information https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md
*/
internal data class CAIP10 constructor(
val chainId: CAIP2,
val accountAddress: String,
) {
val raw get() = "${chainId.raw}$CAIP_SEPARATOR$accountAddress"
companion object {
private const val CAIP10_SUBSTRINGS_SIZE = 3
fun fromRaw(s: String): CAIP10? {
val parsed = s.split(CAIP_SEPARATOR)
if (parsed.size != CAIP10_SUBSTRINGS_SIZE) return null
val namespace = parsed[0]
val reference = parsed[1]
val accountAddress = parsed[2]
val chainId = CAIP2.fromRaw(namespace + CAIP_SEPARATOR + reference) ?: return null
return CAIP10(chainId, accountAddress)
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.data.walletconnect.model
/**
* CAIP-2 defines a way to identify a blockchain (e.g. Ethereum Mainnet, Görli, Bitcoin, Cosmos Hub)
* in a human-readable, developer-friendly and transaction-friendly way.
*
* Syntax
* chain_id: namespace + ":" + reference
* namespace: [-a-z0-9]{3,8}
* reference: [-_a-zA-Z0-9]{1,32}
*
* Examples
* # Ethereum mainnet
* eip155:1
*
* # Bitcoin mainnet
* bip122:000000000019d6689c085ae165831e93
*
* For more information https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md
*/
internal data class CAIP2 constructor(
val namespace: String,
val reference: String,
) {
val raw get() = "$namespace$CAIP_SEPARATOR$reference"
companion object {
const val CAIP_SEPARATOR = ":"
fun fromRaw(s: String): CAIP2? {
val parsed = s.split(CAIP_SEPARATOR)
if (parsed.size != 2) return null
return CAIP2(parsed[0], parsed[1])
}
}
}

View file

@ -0,0 +1,4 @@
package com.tangem.data.walletconnect.model
@JvmInline
internal value class NamespaceKey(val key: String)

View file

@ -0,0 +1,78 @@
package com.tangem.data.walletconnect.network.ethereum
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
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.utils.WcNamespaceConverter
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.respond.WcRespondService
import com.tangem.domain.walletconnect.usecase.WcUseCase
import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider
import com.tangem.domain.walletconnect.usecase.ethereum.EthPersonalSignUseCase
import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod
import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
@Suppress("UnusedPrivateClass") // todo(wc) remove later
internal class WcEthNetwork(
private val moshi: Moshi,
private val respondService: WcRespondService,
) : WcMethodHandler<WcEthMethod>, WcUseCasesFlowProvider, WcNamespaceConverter {
private val _useCases: Channel<WcUseCase> = Channel(Channel.BUFFERED)
override val useCases = _useCases.receiveAsFlow()
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
override fun canHandle(methodName: String): Boolean {
return Name.entries.find { it.raw == methodName } != null
}
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 -> WcMethodHandler.fromJson<SignMessage>(params, moshi)
Name.SignTypeData -> TODO()
Name.SignTypeDataV4 -> TODO()
Name.SignTransaction -> TODO()
Name.SendTransaction -> TODO()
}
}
override fun handle(wcRequest: WcRequest<WcMethod>) {
wcRequest as WcRequest<WcEthMethod>
val useCase = when (wcRequest.method) {
is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest<SignMessage>, respondService)
}
_useCases.trySend(useCase)
}
enum class Name(val raw: String) {
Sign("eth_sign"),
PersonalSign("personal_sign"),
SignTypeData("eth_signTypedData"),
SignTypeDataV4("eth_signTypedData_v4"),
SignTransaction("eth_signTransaction"),
SendTransaction("eth_sendTransaction"),
}
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? {
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.data.walletconnect.network.solana
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
internal class WcSolanaNetwork : WcNamespaceConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
override fun toCAIP2(blockchain: Blockchain): CAIP2? {
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,
)
}
companion object {
private const val MAINNET_CHAIN_ID = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
private const val TESTNET_CHAIN_ID = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"
}
}

View file

@ -0,0 +1,103 @@
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.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.WcNetwork
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
internal class AssociateNetworksDelegate constructor(
private val namespaceConverters: Map<NamespaceKey, 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<UserWalletId, ProposalNetwork> {
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
val requiredNamespaces: Set<CAIP2> = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces: Set<CAIP2> = sessionProposal.optionalNamespaces.setOfChainId()
return userWallets.associate { wallet ->
wallet.walletId to mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces)
}
}
private suspend fun mapNetworksForWallet(
wallet: UserWallet,
requiredNamespaces: Set<CAIP2>,
optionalNamespaces: Set<CAIP2>,
): ProposalNetwork {
val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
val unknownRequired = mutableSetOf<WcNetwork.Unknown>()
val missingRequired = mutableSetOf<WcNetwork.Supported>()
val required = mutableSetOf<WcNetwork.Supported>()
val available = mutableSetOf<WcNetwork.Supported>()
val notAdded = mutableSetOf<WcNetwork.Supported>()
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(WcNetwork.Unknown(missingNetworkName(chainId)))
return@forEach
}
val wcNetwork = blockchain.toNetwork()
if (wcNetwork == null) {
unknownRequired.add(WcNetwork.Unknown(missingNetworkName(blockchain)))
return@forEach
}
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
if (walletNetwork == null) {
missingRequired.add(WcNetwork.Supported(wcNetwork))
} else {
required.add(WcNetwork.Supported(walletNetwork))
}
}
optionalNamespaces.forEach { chainId ->
val wcNetwork = chainId.toBlockchain()?.toNetwork() ?: return@forEach
val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id }
if (walletNetwork == null) {
available.add(WcNetwork.Supported(wcNetwork))
} else {
notAdded.add(WcNetwork.Supported(walletNetwork))
}
}
if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedNetworks(unknownRequired)
return ProposalNetwork(
walletId = wallet.walletId,
missingRequired = missingRequired,
required = required,
available = available,
notAdded = notAdded,
)
}
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 missingNetworkName(chainId: CAIP2): String = chainId.namespace.replaceFirstChar(Char::titlecase)
private fun missingNetworkName(blockchain: Blockchain): String = blockchain.getCoinName()
}

View file

@ -0,0 +1,62 @@
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.UserWalletId
internal class CaipNamespaceDelegate constructor(
private val namespaceConverters: Map<NamespaceKey, WcNamespaceConverter>,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend fun associate(
sessionProposal: Wallet.Model.SessionProposal,
userWalletId: UserWalletId,
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(userWalletId, network)
val chainId = converters.firstOrNull { it.toCAIP2(blockchain) != null }?.toCAIP2(blockchain)
requireNotNull(chainId)
requireNotNull(address)
CAIP10(chainId = chainId, accountAddress = address)
}.forEach { account ->
val namespaceKey = account.chainId.namespace
val session = result.getOrPut(namespaceKey) { Session() }
val requiredNamespaces = sessionProposal.requiredNamespaces
val optionalNamespaces = sessionProposal.optionalNamespaces
val methods = buildSet {
requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) }
optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) }
}
session.chains.add(account.chainId.raw)
session.accounts.add(account.raw)
session.methods.addAll(methods)
}
return result.mapValues { (namespaceKey, session) ->
Wallet.Model.Namespace.Session(
chains = session.chains.toList(),
accounts = session.accounts.toList(),
methods = session.methods.toList(),
events = session.events.toList(),
)
}
}
private data class Session(
val chains: MutableSet<String> = mutableSetOf(),
val accounts: MutableSet<String> = mutableSetOf(),
val methods: MutableSet<String> = mutableSetOf(),
val events: MutableSet<String> = mutableSetOf(),
)
}

View file

@ -7,15 +7,21 @@ import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.toOurModel
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
@ -25,10 +31,10 @@ val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
@Suppress("UnusedPrivateMember") // todo(wc) remove after add mapping
internal class DefaultWcPairUseCase(
private val sessionsManager: WcSessionsManager,
private val associateNetworksDelegate: AssociateNetworksDelegate,
private val caipNamespaceDelegate: CaipNamespaceDelegate,
) : WcPairUseCase, WcSdkObserver {
private val onWalletSelect = Channel<UserWallet>()
private val onAccountSelect = Channel<Any>()
private val onCallTerminalAction = Channel<TerminalAction>()
private val onSessionProposal =
Channel<Pair<Wallet.Model.SessionProposal, Wallet.Model.VerifyContext>>(Channel.BUFFERED)
@ -40,91 +46,66 @@ internal class DefaultWcPairUseCase(
// call sdk.pair and wait result, finish flow on error
walletKitPair(uri).onLeft { throwable ->
emit(WcPairState.Error(throwable))
emit(WcPairState.Error(WcPairError.Unknown(throwable.localizedMessage.orEmpty())))
return@flow
}
// wait for sdk onSessionProposal callback
val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow().map { (fist, second) ->
val ourCopy = WcSdkSessionProposal(
name = fist.name,
description = fist.description,
url = fist.url,
proposerPublicKey = fist.proposerPublicKey,
)
ourCopy to second
}.first { (sessionProposal, verifyContext) ->
true // todo(wc) check verifyContext? compare uri?
}
val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow()
.first { (sessionProposal, verifyContext) ->
true // todo(wc) check verifyContext? compare uri?
}
// check unsupported dApps, just local constant for now, finish if unsupported
if (sdkSessionProposal.name in unsupportedDApps) {
Timber.i("Unsupported DApp")
val error = WcPairState
.Error(RuntimeException("todo(wc) use domain exception")) // todo(wc) WalletConnectError.UnsupportedDApp
val error = WcPairState.Error(WcPairError.UnsupportedDApp)
emit(error)
return@flow
}
// flow that collect terminal and all middle actions
val actionsFlow = channelFlow<TerminalAction> {
val userWallet = onWalletSelect.receiveAsFlow()
.stateIn(scope = this, started = SharingStarted.Lazily, initialValue = selectedWallet)
val accountSelect = onAccountSelect.receiveAsFlow()
.stateIn(scope = this, started = SharingStarted.Lazily, initialValue = Any())
// combine all middle variables and emit new ProposalState to main FlowCollector
combine(accountSelect, userWallet) { account, selectedWallet ->
buildProposalState(sdkSessionProposal, verifyContext, account, selectedWallet)
}.onEach { newProposalState -> this@flow.emit(newProposalState) }
.launchIn(this)
// collect terminal action and emit to this inner channelFlow, unlike combine above
onCallTerminalAction.receiveAsFlow()
.onEach { this.channel.send(it) }
.launchIn(this)
}
val proposalState = buildProposalState(sdkSessionProposal, verifyContext)
.fold(ifLeft = { WcPairState.Error(it) }, ifRight = { it })
emit(proposalState)
// wait first terminal action and continue WC pair flow
val sessionForApprove: WcSessionProposal = when (val terminalAction = actionsFlow.first()) {
val terminalAction = onCallTerminalAction.receiveAsFlow().first()
val sessionForApprove: WcSessionApprove? = when (terminalAction) {
is TerminalAction.Approve -> terminalAction.sessionForApprove
TerminalAction.Reject -> {
// non suspending WalletKit.rejectSession call
rejectSession(sdkSessionProposal)
return@flow
rejectSession(sdkSessionProposal.proposerPublicKey)
null
}
}
// finish flow if rejected above
sessionForApprove ?: return@flow
// start flow of approving in wc sdk
emit(WcPairState.Approving.Loading(sessionForApprove))
fun mapResult(either: Either<Throwable, WcSession>) =
WcPairState.Approving.Result(sessionForApprove, either)
// call sdk approve and wait result
walletKitApproveSession(sessionForApprove).onLeft { emit(mapResult(it.left())) }.onRight {
// wait for sdk callback
val result = when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) {
is Wallet.Model.SettledSessionResponse.Error -> mapResult(
// WalletConnectError.ExternalApprovalError(settledSession.errorMessage) todo(wc)
RuntimeException("todo(wc) use domain exception").left(),
)
val either = walletKitApproveSession(
sessionForApprove = sessionForApprove,
sdkSessionProposal = sdkSessionProposal,
).fold(
ifLeft = { WcPairError.ExternalApprovalError(it.localizedMessage.orEmpty()).left() },
ifRight = {
when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) {
is Wallet.Model.SettledSessionResponse.Error -> WcPairError.ExternalApprovalError(
settledSession.errorMessage,
).left()
is Wallet.Model.SettledSessionResponse.Result -> {
val newSession = settledSession.session.toDomain(sessionForApprove.wallet)
sessionsManager.saveSessions(sessionForApprove.wallet.walletId, newSession)
mapResult(newSession.right())
is Wallet.Model.SettledSessionResponse.Result -> {
val newSession = settledSession.session.toDomain(sessionForApprove.walletId)
sessionsManager.saveSessions(sessionForApprove.walletId, newSession)
newSession.right()
}
}
}
emit(result)
}
},
)
emit(WcPairState.Approving.Result(sessionForApprove, either))
}
override fun onWalletSelect(selectedWallet: UserWallet) {
onWalletSelect.trySend(selectedWallet)
}
override fun onAccountSelect(account: Any) {
onAccountSelect.trySend(account)
}
override fun approve(sessionForApprove: WcSessionProposal) {
override fun approve(sessionForApprove: WcSessionApprove) {
onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove))
}
@ -162,10 +143,22 @@ internal class DefaultWcPairUseCase(
)
}
private suspend fun walletKitApproveSession(sessionForApprove: WcSessionProposal): Either<Throwable, Unit> =
suspendCancellableCoroutine { continuation ->
private suspend fun walletKitApproveSession(
sessionForApprove: WcSessionApprove,
sdkSessionProposal: Wallet.Model.SessionProposal,
): Either<Throwable, Unit> {
val namespaces = caipNamespaceDelegate.associate(
sdkSessionProposal,
sessionForApprove.walletId,
sessionForApprove.network.map { it.network },
)
val sessionApprove = Wallet.Params.SessionApprove(
proposerPublicKey = sdkSessionProposal.proposerPublicKey,
namespaces = namespaces,
)
return suspendCancellableCoroutine { continuation ->
WalletKit.approveSession(
params = TODO("wc sdk model"),
params = sessionApprove,
onSuccess = {
Timber.i("Approved successfully: $it")
continuation.resume(Unit.right())
@ -176,11 +169,12 @@ internal class DefaultWcPairUseCase(
},
)
}
}
private fun rejectSession(sessionProposal: WcSdkSessionProposal) {
private fun rejectSession(proposerPublicKey: String) {
WalletKit.rejectSession(
params = Wallet.Params.SessionReject(
proposerPublicKey = sessionProposal.proposerPublicKey,
proposerPublicKey = proposerPublicKey,
reason = "",
),
onSuccess = {
@ -192,25 +186,38 @@ internal class DefaultWcPairUseCase(
)
}
private fun buildProposalState(
sessionProposal: WcSdkSessionProposal,
private suspend fun buildProposalState(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
selectedAccount: Any,
userWallet: UserWallet,
): WcPairState.Proposal {
// todo(wc) a lot of mapping from Wallet.Model.SessionProposal to our Blockchain, Network, ect
// todo(wc) check security status by Wallet.Model.VerifyContext or Blockaid, don't know for now
val mock: WcSessionProposal = TODO()
return WcPairState.Proposal(mock, Any())
}
): Either<WcPairError, WcPairState.Proposal> = runCatching {
val proposalNetwork = associateNetworksDelegate.associate(sessionProposal)
val appMetaData = WcAppMetaData(
name = sessionProposal.name,
description = sessionProposal.description,
url = sessionProposal.url,
icons = sessionProposal.icons.map { it.toString() },
redirect = sessionProposal.redirect,
)
val dAppSession = WcSessionProposal(
dAppMetaData = appMetaData,
proposalNetwork = proposalNetwork,
securityStatus = Any(),
)
WcPairState.Proposal(dAppSession)
}.fold(onSuccess = { it.right() }, onFailure = {
when (it) {
is WcPairError -> it.left()
else -> WcPairError.Unknown(it.localizedMessage.orEmpty()).left()
}
},)
private fun Wallet.Model.Session.toDomain(userWallet: UserWallet): WcSession = WcSession(
userWalletId = userWallet.walletId,
private fun Wallet.Model.Session.toDomain(walletId: UserWalletId): WcSession = WcSession(
userWalletId = walletId,
sdkModel = this.toOurModel(),
)
private sealed interface TerminalAction {
data class Approve(val sessionForApprove: WcSessionProposal) : TerminalAction
data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction
data object Reject : TerminalAction
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.data.walletconnect.utils.toOurModel
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.WcRequestHandler
import com.tangem.domain.walletconnect.request.WcRequestService
import com.tangem.domain.walletconnect.respond.WcRespondService
import kotlinx.coroutines.CoroutineScope
@ -16,7 +15,7 @@ import kotlinx.coroutines.launch
internal class DefaultWcRequestService(
private val sessionsManager: WcSessionsManager,
private val respondService: WcRespondService,
private val requestAdapters: List<WcRequestHandler<WcMethod>>,
private val requestAdapters: Set<WcMethodHandler<WcMethod>>,
private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope
) : WcRequestService, WcSdkObserver {
@ -32,7 +31,7 @@ internal class DefaultWcRequestService(
val params = sr.request.params
scope.launch {
val session = sessionsManager.findSessionByTopic(sr.topic)
val handler: WcRequestHandler<WcMethod>? = requestAdapters.firstOrNull { it.canHandle(method) }
val handler: WcMethodHandler<WcMethod>? = requestAdapters.firstOrNull { it.canHandle(method) }
val deserialized: WcMethod? = handler?.deserialize(method, params)
if (handler == null || deserialized == null || session == null) {

View file

@ -0,0 +1,18 @@
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

@ -1,4 +1,4 @@
package com.tangem.data.walletconnect
package com.tangem.data.walletconnect.sessions
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.utils.WcSdkObserver

View file

@ -0,0 +1,12 @@
package com.tangem.data.walletconnect.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
internal interface WcNamespaceConverter {
val namespaceKey: NamespaceKey
fun toBlockchain(chainId: CAIP2): Blockchain?
fun toCAIP2(blockchain: Blockchain): CAIP2?
}