Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-02 14:24:40 +07:00
parent ac6f73170e
commit bdcd8f2ce8
25 changed files with 224 additions and 30 deletions

View file

@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.domain.walletconnect.model.legacy.Account
internal class TangemWcBlockchainHelper : WcBlockchainHelper {

View file

@ -9,6 +9,7 @@ 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.unsupportedDApps
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository

View file

@ -4,8 +4,8 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.models.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.model.legacy.Session
import timber.log.Timber
internal class DefaultWalletConnectSessionsRepository(

View file

@ -7,6 +7,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.flow.Flow

View file

@ -6,6 +6,9 @@ import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.domain.walletconnect.model.legacy.Account
interface WcBlockchainHelper {
fun chainIdToNetworkIdOrNull(chainId: String): String?

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain.models
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import java.net.URI

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.domain.onramp.models)
implementation(projects.domain.models)
implementation(projects.domain.nft.models)
implementation(projects.domain.walletConnect.models)
/** Tangem libraries */
implementation(tangemDeps.blockchain)

View file

@ -0,0 +1,45 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.walletconnect.DefaultWalletConnectStore
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.setTypes
import com.tangem.domain.walletconnect.model.WcSessionDTO
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object WalletConnectModule {
@Provides
@Singleton
fun provideWalletConnectStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): WalletConnectStore {
return DefaultWalletConnectStore(
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = setTypes<WcSessionDTO>(),
defaultValue = emptySet(),
),
produceFile = { context.dataStoreFile(fileName = "wallet_connect_sessions") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.local.walletconnect
import androidx.datastore.core.DataStore
import com.tangem.domain.walletconnect.model.WcSessionDTO
import kotlinx.coroutines.flow.Flow
internal typealias WcSessionCollection = Set<WcSessionDTO>
class DefaultWalletConnectStore(
private val persistenceStore: DataStore<WcSessionCollection>,
) : WalletConnectStore {
override val sessions: Flow<WcSessionCollection>
get() = persistenceStore.data
override suspend fun saveSessions(sessions: WcSessionCollection) {
persistenceStore.updateData { data -> data.plus(sessions) }
}
override suspend fun removeSessions(sessions: WcSessionCollection) {
persistenceStore.updateData { data -> data.minus(sessions) }
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.local.walletconnect
import com.tangem.domain.walletconnect.model.WcSessionDTO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
interface WalletConnectStore {
val sessions: Flow<WcSessionCollection>
suspend fun findSessionByTopic(topic: String) = sessions.first().find { it.topic == topic }
suspend fun saveSessions(sessions: WcSessionCollection)
suspend fun saveSession(session: WcSessionDTO) = saveSessions(setOf(session))
suspend fun removeSessions(sessions: WcSessionCollection)
suspend fun removeSession(session: WcSessionDTO) = removeSessions(setOf(session))
}

View file

@ -18,8 +18,10 @@ 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.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
@ -82,7 +84,21 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun defaultWcSessionsManager(): DefaultWcSessionsManager = DefaultWcSessionsManager()
fun defaultWcSessionsManager(
store: WalletConnectStore,
dispatchers: CoroutineDispatcherProvider,
legacyStore: WalletConnectSessionsRepository,
getWallets: GetWalletsUseCase,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcSessionsManager(
store = store,
dispatchers = dispatchers,
legacyStore = legacyStore,
getWallets = getWallets,
scope = scope,
)
}
@Provides
@Singleton

View file

@ -96,7 +96,7 @@ internal class DefaultWcPairUseCase(
is Wallet.Model.SettledSessionResponse.Result -> {
val newSession = settledSession.session.toDomain(sessionForApprove.walletId)
sessionsManager.saveSessions(sessionForApprove.walletId, newSession)
sessionsManager.saveSession(sessionForApprove.walletId, newSession)
newSession.right()
}
}

View file

@ -1,38 +1,92 @@
package com.tangem.data.walletconnect.sessions
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.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionDTO
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultWcSessionsManager : WcSessionsManager, WcSdkObserver {
internal class DefaultWcSessionsManager constructor(
private val store: WalletConnectStore,
private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
private val _sessions = MutableSharedFlow<Map<UserWalletId, List<WcSession>>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val sessions get() = _sessions.distinctUntilChanged()
override val sessions: Flow<Map<UserWalletId, List<WcSession>>>
get() = store.sessions
.onEach(::migrateLegacyStore)
.map(::associateWithSdk)
.distinctUntilChanged()
.flowOn(dispatchers.io)
override suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) {
TODO("Not yet implemented")
override suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) {
store.saveSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId))
}
override suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) {
TODO("Not yet implemented")
override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) {
store.removeSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId))
}
override suspend fun findSessionByTopic(topic: String): WcSession? {
TODO("Not yet implemented")
override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) {
val storedSessions = store.findSessionByTopic(topic) ?: return@withContext null
val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null
WcSession(userWalletId = storedSessions.walletId, sdkModel = sdkSession.toOurModel())
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
// Triggered when the session is deleted by the peer
if (sessionDelete !is Wallet.Model.SessionDelete.Success) return
Timber.i("onSessionDelete: $sessionDelete")
// todo (wc) find session, delete and update sessions Flow
scope.launch {
val storedSessions = store.findSessionByTopic(sessionDelete.topic) ?: return@launch
store.removeSession(storedSessions)
}
}
private suspend fun migrateLegacyStore(inNewStoreSessions: Set<WcSessionDTO>) {
val walletIds = getWallets.invokeSync().mapTo(mutableSetOf()) { it.walletId }
val inLegacyStoreSessions = walletIds
.map { walletId ->
flow { emit(legacyStore.loadSessions(walletId.stringValue).map { WcSessionDTO(it.topic, walletId) }) }
}
.merge()
.first()
val mustSaveInNewStore = inLegacyStoreSessions.subtract(inNewStoreSessions)
if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore)
}
private suspend fun associateWithSdk(storeSessions: Set<WcSessionDTO>): Map<UserWalletId, List<WcSession>> {
val sdkSessions = WalletKit.getListOfActiveSessions()
val wcSessions = sdkSessions.mapNotNull { sdkSession ->
val storedSessions = storeSessions.find { it.topic == sdkSession.topic }
?: return@mapNotNull null
WcSession(userWalletId = storedSessions.walletId, sdkModel = sdkSession.toOurModel())
}
val unknownStoredSessions = storeSessions
.filterNot { dto -> wcSessions.any { it.sdkModel.topic == dto.topic } }
if (unknownStoredSessions.isNotEmpty()) {
unknownStoredSessions.forEach { unknown ->
legacyStore.removeSession(unknown.walletId.stringValue, unknown.topic)
}
store.removeSessions(unknownStoredSessions.toSet())
}
return wcSessions.groupBy { it.userWalletId }
}
}

View file

@ -1,12 +1,15 @@
package com.tangem.data.walletconnect.utils
import com.reown.android.Core
import com.reown.walletkit.client.Wallet
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest
internal fun Wallet.Model.Session.toOurModel(): WcSdkSession = WcSdkSession(
topic = topic,
appMetaData = metaData.toOurModel(),
)
internal fun Wallet.Model.SessionRequest.toOurModel(): WcSdkSessionRequest = WcSdkSessionRequest(
@ -19,4 +22,15 @@ internal fun Wallet.Model.SessionRequest.JSONRPCRequest.toOurModel(): JSONRPCReq
id = this.id,
method = this.method,
params = this.params,
)
internal fun Core.Model.AppMetaData?.toOurModel(): WcAppMetaData = WcAppMetaData(
name = this?.name.orEmpty(),
description = this?.description.orEmpty(),
url = this?.url.orEmpty(),
icons = this?.icons.orEmpty(),
redirect = this?.redirect,
appLink = this?.appLink,
linkMode = this?.linkMode ?: false,
verifyUrl = this?.verifyUrl,
)

View file

@ -1,5 +1,6 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.ksp)
id("configuration")
}
dependencies {
@ -7,4 +8,8 @@ dependencies {
/* Domain */
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
/* Other */
implementation(deps.moshi)
ksp(deps.moshi.kotlin.codegen)
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.walletconnect.model
import com.squareup.moshi.JsonClass
import com.tangem.domain.wallets.models.UserWalletId
@JsonClass(generateAdapter = true)
data class WcSessionDTO(
val topic: String,
val walletId: UserWalletId,
)

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.walletconnect2.domain.models
package com.tangem.domain.walletconnect.model.legacy
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.walletconnect2.domain.models
package com.tangem.domain.walletconnect.model.legacy
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,6 +1,4 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.domain.walletconnect2.domain.models.Session
package com.tangem.domain.walletconnect.model.legacy
interface WalletConnectSessionsRepository {
suspend fun loadSessions(userWallet: String): List<Session>

View file

@ -5,4 +5,5 @@ package com.tangem.domain.walletconnect.model.sdkcopy
*/
data class WcSdkSession(
val topic: String,
val appMetaData: WcAppMetaData,
)

View file

@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
interface WcSessionsManager {
val sessions: Flow<Map<UserWalletId, List<WcSession>>>
suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession)
suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession)
suspend fun saveSession(userWalletId: UserWalletId, session: WcSession)
suspend fun removeSession(userWalletId: UserWalletId, session: WcSession)
suspend fun findSessionByTopic(topic: String): WcSession?
}

View file

@ -13,6 +13,8 @@ android {
dependencies {
implementation(projects.features.walletconnect.api)
implementation(projects.domain.walletConnect)
implementation(projects.domain.walletConnect.models)
/** Core */
implementation(projects.core.configToggles)