From b73b6c3fe70998cd257bda38f41d51f2f215942c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 16:04:08 +0700 Subject: [PATCH] Updated on 2026-08-14 --- .../initialize/DefaultWcInitializeUseCase.kt | 1 + .../sessions/DefaultWcSessionsManager.kt | 93 +++++++++++++++---- .../data/walletconnect/utils/WcSdkObserver.kt | 2 + .../repository/WcSessionsManager.kt | 3 +- .../usecase/WcSessionsUseCase.kt | 17 ++++ .../usecase/disconnect/WcDisconnectUseCase.kt | 30 ++++++ 6 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index 5889dbf628..6330859a84 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -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") diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 2807f9d74c..90ccec710d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -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(capacity = Channel.BUFFERED) + private val oneTimeMigration = MutableStateFlow(true) + override val sessions: Flow>> 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 = WalletKit.getListOfActiveSessions() + val associatedSessions: List = 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 { + 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) { + private suspend fun migrateLegacyStore(inNewStoreSessions: Set): 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): Map> { - val sdkSessions = WalletKit.getListOfActiveSessions() + private fun associateWithSdk( + sdkSessions: List, + storeSessions: Set, + ): List { 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, wcSessions: List): 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 { + 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() + .mapNotNull { sessionDelete -> store.findSessionByTopic(sessionDelete.topic) } + .onEach { sessionDto -> store.removeSession(sessionDto) } + .flowOn(dispatchers.io) + .launchIn(scope) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt index 5c15ea1b80..4c70570c4d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt @@ -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) {} diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt index 7deafa02b2..6c2a996c91 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -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>> suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) - suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) + suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either suspend fun findSessionByTopic(topic: String): WcSession? } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt new file mode 100644 index 0000000000..0adbf1dff6 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt @@ -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>> { + return sessionsManager.sessions + } + + suspend fun invokeSync(): Map> { + return sessionsManager.sessions.first() + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt new file mode 100644 index 0000000000..a34bd8e9f6 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt @@ -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) + } +} \ No newline at end of file