Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-03 16:04:08 +07:00
parent d5fdc0d921
commit b73b6c3fe7
6 changed files with 128 additions and 18 deletions

View file

@ -55,6 +55,7 @@ internal class DefaultWcInitializeUseCase(
onSuccess = {
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
wcSdkObservers.forEach { it.onWcSdkInit() }
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")

View file

@ -1,5 +1,8 @@
package com.tangem.data.walletconnect.sessions
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WcSdkObserver
@ -13,10 +16,14 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
internal class DefaultWcSessionsManager constructor(
private val store: WalletConnectStore,
@ -26,19 +33,50 @@ internal class DefaultWcSessionsManager constructor(
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
private val oneTimeMigration = MutableStateFlow(true)
override val sessions: Flow<Map<UserWalletId, List<WcSession>>>
get() = store.sessions
.onEach(::migrateLegacyStore)
.map(::associateWithSdk)
.distinctUntilChanged()
.transform { inStore ->
if (oneTimeMigration.value) {
oneTimeMigration.value = false
val someMigrated = migrateLegacyStore(inStore)
if (someMigrated) return@transform // ignore emit, wait next one
}
val inSdk: List<Wallet.Model.Session> = WalletKit.getListOfActiveSessions()
val associatedSessions: List<WcSession> = associateWithSdk(inSdk, inStore)
val someRemove = removeUnknownSessions(inStore, associatedSessions)
if (someRemove) return@transform // ignore emit, wait next one
emit(associatedSessions.groupBy { it.userWalletId })
}
.flowOn(dispatchers.io)
override fun onWcSdkInit() {
oneTimeMigration.value = true
listenOnSessionDelete()
}
override suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) {
store.saveSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId))
}
override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) {
store.removeSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId))
override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either<Throwable, Unit> {
val topic = session.sdkModel.topic
val sdkCall = sdkDisconnectSession(topic)
sdkCall.onLeft { return it.left() }
suspend fun waitSdkCallback() = onSessionDelete.receiveAsFlow().first {
val isSomeError = it is Wallet.Model.SessionDelete.Error
val isDeleted = it is Wallet.Model.SessionDelete.Success && it.topic == topic
isSomeError || isDeleted
}
val waitSdkCallback = runCatching { withTimeout(10.seconds) { waitSdkCallback() } }
val sdkCallback = waitSdkCallback.getOrElse { return it.left() }
return when (sdkCallback) {
is Wallet.Model.SessionDelete.Error -> sdkCallback.error.left()
is Wallet.Model.SessionDelete.Success -> Unit.right()
}
}
override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) {
@ -48,45 +86,66 @@ internal class DefaultWcSessionsManager constructor(
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
if (sessionDelete !is Wallet.Model.SessionDelete.Success) return
Timber.i("onSessionDelete: $sessionDelete")
scope.launch {
val storedSessions = store.findSessionByTopic(sessionDelete.topic) ?: return@launch
store.removeSession(storedSessions)
}
onSessionDelete.trySend(sessionDelete)
}
private suspend fun migrateLegacyStore(inNewStoreSessions: Set<WcSessionDTO>) {
private suspend fun migrateLegacyStore(inNewStoreSessions: Set<WcSessionDTO>): Boolean {
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()
.reduce { accumulator, value -> accumulator.plus(value) }
val mustSaveInNewStore = inLegacyStoreSessions.subtract(inNewStoreSessions)
if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore)
return mustSaveInNewStore.isNotEmpty()
}
private suspend fun associateWithSdk(storeSessions: Set<WcSessionDTO>): Map<UserWalletId, List<WcSession>> {
val sdkSessions = WalletKit.getListOfActiveSessions()
private fun associateWithSdk(
sdkSessions: List<Wallet.Model.Session>,
storeSessions: Set<WcSessionDTO>,
): List<WcSession> {
val wcSessions = sdkSessions.mapNotNull { sdkSession ->
val storedSessions = storeSessions.find { it.topic == sdkSession.topic }
?: return@mapNotNull null
WcSession(userWalletId = storedSessions.walletId, sdkModel = WcSdkSessionConverter.convert(sdkSession))
}
return wcSessions
}
private suspend fun removeUnknownSessions(storeSessions: Set<WcSessionDTO>, wcSessions: List<WcSession>): Boolean {
val unknownStoredSessions = storeSessions
.filterNot { dto -> wcSessions.any { it.sdkModel.topic == dto.topic } }
val haveSomeUnknown = unknownStoredSessions.isNotEmpty()
if (unknownStoredSessions.isNotEmpty()) {
if (haveSomeUnknown) {
unknownStoredSessions.forEach { unknown ->
legacyStore.removeSession(unknown.walletId.stringValue, unknown.topic)
}
store.removeSessions(unknownStoredSessions.toSet())
}
return haveSomeUnknown
}
return wcSessions.groupBy { it.userWalletId }
private suspend fun sdkDisconnectSession(topic: String): Either<Throwable, Unit> {
return suspendCancellableCoroutine { continuation ->
WalletKit.disconnectSession(
params = Wallet.Params.SessionDisconnect(topic),
onSuccess = { continuation.resume(Unit.right()) },
onError = { continuation.resume(it.throwable.left()) },
)
}
}
private fun listenOnSessionDelete() {
onSessionDelete.receiveAsFlow()
.filterIsInstance<Wallet.Model.SessionDelete.Success>()
.mapNotNull { sessionDelete -> store.findSessionByTopic(sessionDelete.topic) }
.onEach { sessionDto -> store.removeSession(sessionDto) }
.flowOn(dispatchers.io)
.launchIn(scope)
}
}

View file

@ -8,6 +8,8 @@ internal interface WcSdkObserver : WalletKit.WalletDelegate {
override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)?
get() = super.onSessionAuthenticate
fun onWcSdkInit() {}
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {}
override fun onError(error: Wallet.Model.Error) {}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.walletconnect.repository
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -7,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
interface WcSessionsManager {
val sessions: Flow<Map<UserWalletId, List<WcSession>>>
suspend fun saveSession(userWalletId: UserWalletId, session: WcSession)
suspend fun removeSession(userWalletId: UserWalletId, session: WcSession)
suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either<Throwable, Unit>
suspend fun findSessionByTopic(topic: String): WcSession?
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.walletconnect.usecase
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
class WcSessionsUseCase(private val sessionsManager: WcSessionsManager) {
operator fun invoke(): Flow<Map<UserWalletId, List<WcSession>>> {
return sessionsManager.sessions
}
suspend fun invokeSync(): Map<UserWalletId, List<WcSession>> {
return sessionsManager.sessions.first()
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.domain.walletconnect.usecase.disconnect
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.toList
class WcDisconnectUseCase(
private val sessionsManager: WcSessionsManager,
) {
suspend fun disconnectAll() {
sessionsManager.sessions.first()
.flatMap { it.value }
.map { session -> flow { emit(disconnect(session)) } }
.merge()
.toList()
}
suspend fun disconnect(topic: String) {
val session = sessionsManager.findSessionByTopic(topic) ?: return
disconnect(session)
}
suspend fun disconnect(session: WcSession) {
sessionsManager.removeSession(session.userWalletId, session)
}
}