From 8bfc3d7f8cac6ac1d3e7b27ace41f7c05e62a727 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Sep 2025 20:30:56 +0700 Subject: [PATCH] Updated on 2026-08-14 --- .../datasource/di/WalletConnectModule.kt | 13 +++- .../DefaultWalletConnectStore.kt | 33 +++++++++ .../local/walletconnect/WalletConnectStore.kt | 6 ++ .../di/WalletConnectDataModule.kt | 16 ++-- .../ethereum/WcEthSendTransactionUseCase.kt | 7 +- .../ethereum/WcEthSignTransactionUseCase.kt | 7 +- .../pair/DefaultWcPairUseCase.kt | 48 +++++++----- .../walletconnect/pair/WcPairSdkDelegate.kt | 73 +++++++++---------- .../sessions/DefaultWcSessionsManager.kt | 36 ++++----- .../data/walletconnect/utils/WcScope.kt | 13 ++++ .../walletconnect/DefaultWcPairUseCaseTest.kt | 27 ++----- .../walletconnect/model/WcSessionDTO.kt | 11 +++ .../repository/WcSessionsManager.kt | 1 - .../method/BlockAidTransactionCheck.kt | 11 +-- .../usecase/pair/WcPairUseCase.kt | 3 +- .../connections/model/WcPairModel.kt | 8 +- 16 files changed, 182 insertions(+), 131 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcScope.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt index 4c5f149013..e790ce4dc7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt @@ -8,6 +8,7 @@ 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.WcPendingApprovalSessionDTO import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -30,6 +31,7 @@ object WalletConnectModule { @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, ): WalletConnectStore { + val scope = CoroutineScope(context = dispatchers.io + SupervisorJob()) return DefaultWalletConnectStore( persistenceStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( @@ -38,7 +40,16 @@ object WalletConnectModule { defaultValue = emptySet(), ), produceFile = { context.dataStoreFile(fileName = "wallet_connect_sessions") }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + scope = scope, + ), + pendingApprovalSessionsStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = setTypes(), + defaultValue = emptySet(), + ), + produceFile = { context.dataStoreFile(fileName = "wallet_connect_pending_approval_sessions") }, + scope = scope, ), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt index 79da8620de..d63e5bb1b6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt @@ -1,18 +1,39 @@ package com.tangem.datasource.local.walletconnect import androidx.datastore.core.DataStore +import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO import com.tangem.domain.walletconnect.model.WcSessionDTO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.transform +import org.joda.time.DateTime internal typealias WcSessionCollection = Set internal class DefaultWalletConnectStore( private val persistenceStore: DataStore, + private val pendingApprovalSessionsStore: DataStore>, ) : WalletConnectStore { override val sessions: Flow get() = persistenceStore.data + override val pendingApproval: Flow> + get() = pendingApprovalSessionsStore.data + .transform { pendingApprovalSet -> + val now = DateTime.now() + val expired = pendingApprovalSet + .filterTo(mutableSetOf()) { it.expiredTime < now.millis } + val shouldSomeClear = expired.isNotEmpty() + val actualData = if (shouldSomeClear) { + removePendingApproval(expired) + } else { + pendingApprovalSet + } + emit(actualData) + } + .distinctUntilChanged() + override suspend fun saveSessions(sessions: WcSessionCollection) { persistenceStore.updateData { data -> data.plus(sessions) } } @@ -20,4 +41,16 @@ internal class DefaultWalletConnectStore( override suspend fun removeSessions(sessions: WcSessionCollection) { persistenceStore.updateData { data -> data.minus(sessions) } } + + override suspend fun savePendingApproval( + sessions: Set, + ): Set { + return pendingApprovalSessionsStore.updateData { data -> data.plus(sessions) } + } + + override suspend fun removePendingApproval( + sessions: Set, + ): Set { + return pendingApprovalSessionsStore.updateData { data -> data.minus(sessions) } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt index 4f3a8397b8..baad949d54 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.local.walletconnect +import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO import com.tangem.domain.walletconnect.model.WcSessionDTO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -7,6 +8,8 @@ import kotlinx.coroutines.flow.first interface WalletConnectStore { val sessions: Flow + val pendingApproval: Flow> + suspend fun findSessionByTopic(topic: String) = sessions.first().find { it.topic == topic } suspend fun saveSessions(sessions: WcSessionCollection) @@ -14,4 +17,7 @@ interface WalletConnectStore { suspend fun removeSessions(sessions: WcSessionCollection) suspend fun removeSession(session: WcSessionDTO) = removeSessions(setOf(session)) + + suspend fun savePendingApproval(sessions: Set): Set + suspend fun removePendingApproval(sessions: Set): Set } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index a83cefd1ae..780142a298 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -17,6 +17,7 @@ import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.data.walletconnect.utils.WcNetworksConverter +import com.tangem.data.walletconnect.utils.WcScope import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore @@ -36,8 +37,6 @@ 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 @@ -80,7 +79,10 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun sdkDelegate(): WcPairSdkDelegate = WcPairSdkDelegate() + fun sdkDelegate(wcScope: WcScope, store: WalletConnectStore): WcPairSdkDelegate = WcPairSdkDelegate( + scope = wcScope, + store = store, + ) @Provides @Singleton @@ -90,15 +92,15 @@ internal object WalletConnectDataModule { getWallets: GetWalletsUseCase, wcNetworksConverter: WcNetworksConverter, analytics: AnalyticsEventHandler, + wcScope: WcScope, ): DefaultWcSessionsManager { - val scope = CoroutineScope(SupervisorJob() + dispatchers.io) return DefaultWcSessionsManager( store = store, dispatchers = dispatchers, getWallets = getWallets, wcNetworksConverter = wcNetworksConverter, analytics = analytics, - scope = scope, + scope = wcScope, ) } @@ -106,6 +108,10 @@ internal object WalletConnectDataModule { @Singleton fun wcSessionsManager(default: DefaultWcSessionsManager): WcSessionsManager = default + @Provides + @Singleton + fun wcScope(dispatchers: CoroutineDispatcherProvider): WcScope = WcScope(dispatchers) + @Provides @Singleton fun wcRequestService(default: DefaultWcRequestService): WcRequestService = default diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index e82c497072..165fe556d9 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -74,12 +74,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( logoUrl = tokenInfo.logoUrl, chainId = tokenInfo.chainId, ) - BlockAidTransactionCheck.Result.Approval( - result = result, - approval = this, - tokenInfo = tokenInfo, - isMutable = true, - ) + BlockAidTransactionCheck.Result.Approval(result = result) } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index 8cfe678df0..e71f37533d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -68,12 +68,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( chainId = tokenInfo.chainId, ) } - BlockAidTransactionCheck.Result.Approval( - result = result, - approval = this, - tokenInfo = tokenInfo, - isMutable = true, - ) + BlockAidTransactionCheck.Result.Approval(result = result) } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 2383bb2e9d..529f7a9302 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -9,13 +9,11 @@ import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.WC_TAG -import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.data.walletconnect.utils.getDappOriginUrl import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.* 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 dagger.assisted.Assisted @@ -25,12 +23,12 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime +import org.joda.time.Duration import timber.log.Timber import java.net.URI @Suppress("LongParameterList") internal class DefaultWcPairUseCase @AssistedInject constructor( - private val sessionsManager: WcSessionsManager, private val associateNetworksDelegate: AssociateNetworksDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate, private val sdkDelegate: WcPairSdkDelegate, @@ -105,24 +103,29 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( // start flow of approving in wc sdk emit(WcPairState.Approving.Loading(sessionForApprove)) + + val connectingTime = DateTime.now().millis + val expiredTime = connectingTime + Duration + .standardMinutes(PENDING_SESSION_EXPIRED_DURATION_MIN) + .millis + val sessionDTO = WcSessionDTO( + topic = "", + walletId = sessionForApprove.wallet.walletId, + url = sdkVerifyContext.getDappOriginUrl(), + securityStatus = proposalState.dAppSession.securityStatus, + connectingTime = connectingTime, + ) + val pendingSessionForSave = WcPendingApprovalSessionDTO( + pairingTopic = sdkSessionProposal.pairingTopic, + session = sessionDTO, + expiredTime = expiredTime, + ) + val either = walletKitApproveSession( + pendingSessionForSave = pendingSessionForSave, sessionForApprove = sessionForApprove, sdkSessionProposal = sdkSessionProposal, ).map { settledSession -> - val newSession = WcSession( - wallet = sessionForApprove.wallet, - sdkModel = WcSdkSessionConverter.convert( - value = WcSdkSessionConverter.Input( - originUrl = sdkVerifyContext.getDappOriginUrl(), - session = settledSession.session, - ), - ), - securityStatus = proposalState.dAppSession.securityStatus, - networks = sessionForApprove.network.toSet(), - connectingTime = DateTime.now().millis, - showWalletInfo = proposalState.dAppSession.proposalNetwork.keys.size > 1, - ) - sessionsManager.saveSession(newSession) analytics.send( WcAnalyticEvents.DAppConnected( sessionProposal = proposalState.dAppSession, @@ -130,7 +133,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( securityStatus = proposalState.dAppSession.securityStatus, ), ) - newSession + proposalState.dAppSession.dAppMetaData }.onLeft { analytics.send( WcAnalyticEvents.DAppConnectionFailed( @@ -170,9 +173,10 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } private suspend fun walletKitApproveSession( + pendingSessionForSave: WcPendingApprovalSessionDTO, sessionForApprove: WcSessionApprove, sdkSessionProposal: Wallet.Model.SessionProposal, - ): Either = try { + ): Either = try { val namespaces = caipNamespaceDelegate.associate( sdkSessionProposal, sessionForApprove, @@ -181,7 +185,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( proposerPublicKey = sdkSessionProposal.proposerPublicKey, namespaces = namespaces, ) - sdkDelegate.approve(sessionApprove) + sdkDelegate.approve(pendingSessionForSave, sessionApprove) } catch (e: Throwable) { Timber.tag(WC_TAG).e(e, "Failed to sdk approve session $pairRequest") WcPairError.ApprovalFailed(e.message.orEmpty()).left() @@ -233,6 +237,10 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( }, ) + private companion object { + const val PENDING_SESSION_EXPIRED_DURATION_MIN = 15L + } + private sealed interface TerminalAction { data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction data object Reject : TerminalAction diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 2e71915a53..a9d9030333 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -6,25 +6,45 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WC_TAG +import com.tangem.data.walletconnect.utils.WcScope import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.data.walletconnect.utils.getDappOriginUrl import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed +import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.* import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds -internal class WcPairSdkDelegate : WcSdkObserver { +internal class WcPairSdkDelegate( + private val scope: WcScope, + private val store: WalletConnectStore, +) : WcSdkObserver { private val onSessionProposal = Channel>() private val onSdkErrorCallback = Channel() - private val onSessionSettleResponse = Channel() + private val onSessionSettleCallback = Channel() + + init { + onSessionSettleCallback.receiveAsFlow() + .filterIsInstance() + .buffer() + .onEach { settledResponse -> + val savedPending = store.pendingApproval.first() + val settledSession = settledResponse.session + val savedPendingSession = savedPending + .find { it.pairingTopic == settledSession.pairingTopic } + ?: return@onEach + store.saveSession(savedPendingSession.session.copy(topic = settledSession.topic)) + store.removePendingApproval(setOf(savedPendingSession)) + } + .launchIn(scope) + } suspend fun pair( url: String, @@ -56,43 +76,20 @@ internal class WcPairSdkDelegate : WcSdkObserver { }.first() suspend fun approve( + pendingSessionForSave: WcPendingApprovalSessionDTO, sessionApprove: Wallet.Params.SessionApprove, - ): Either = coroutineScope { - val approveCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { approveCallback() } } - val approveCall = async { sdkApprove(sessionApprove) } - approveCall.await() - .onLeft { - approveCallback.cancel() - return@coroutineScope it.left() - } - return@coroutineScope approveCallback.await().fold( - ifLeft = { it.left() }, - ifRight = { result -> - when (result) { - is Wallet.Model.SettledSessionResponse.Result -> result.right() - is Wallet.Model.SettledSessionResponse.Error -> ApprovalFailed(result.errorMessage).left() - } + ): Either = coroutineScope { + val forSave = setOf(pendingSessionForSave) + store.savePendingApproval(forSave) + sdkApprove(sessionApprove).fold( + ifRight = { Unit.right() }, + ifLeft = { + store.removePendingApproval(forSave) + it.left() }, ) } - private suspend fun approveCallback() = callbackFlow> { - // wait first onSessionSettleResponse callback - launch { - val settledSessionResponse = onSessionSettleResponse.receiveAsFlow().first() - trySend(settledSessionResponse.right()) - channel.close() - } - // OR - // wait first onError callback - launch { - val error = onSdkErrorCallback.receiveAsFlow().first() - trySend(error.throwable.toApproveError()) - channel.close() - } - awaitClose() - }.first() - fun rejectSession(proposerPublicKey: String) { Timber.tag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey") WalletKit.rejectSession( @@ -120,7 +117,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { // Triggered when wallet receives the session settlement response from Dapp - onSessionSettleResponse.trySend(settleSessionResponse) + onSessionSettleCallback.trySend(settleSessionResponse) } private suspend fun sdkApprove(sessionApprove: Wallet.Params.SessionApprove): Either { 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 8697778af1..6786e8a318 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 @@ -6,10 +6,7 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.data.walletconnect.utils.WC_TAG -import com.tangem.data.walletconnect.utils.WcNetworksConverter -import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.data.walletconnect.utils.WcSdkSessionConverter +import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.WcAnalyticEvents @@ -18,10 +15,12 @@ import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import org.joda.time.DateTime +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.coroutines.resume @@ -32,7 +31,7 @@ internal class DefaultWcSessionsManager( private val dispatchers: CoroutineDispatcherProvider, private val wcNetworksConverter: WcNetworksConverter, private val analytics: AnalyticsEventHandler, - private val scope: CoroutineScope, + private val scope: WcScope, ) : WcSessionsManager, WcSdkObserver { private val onSessionDelete = Channel(capacity = Channel.BUFFERED) @@ -55,18 +54,6 @@ internal class DefaultWcSessionsManager( extendSessions() } - override suspend fun saveSession(session: WcSession) { - store.saveSession( - WcSessionDTO( - topic = session.sdkModel.topic, - walletId = session.wallet.walletId, - url = session.sdkModel.appMetaData.url, - securityStatus = session.securityStatus, - connectingTime = session.connectingTime ?: DateTime.now().millis, - ), - ) - } - override suspend fun removeSession(session: WcSession): Either { val topic = session.sdkModel.topic val sdkCall = sdkDisconnectSession(topic) @@ -91,7 +78,16 @@ internal class DefaultWcSessionsManager( inStore: Set, wallets: List, ): List { - val wcSessions = inStore.mapNotNull { storeSession -> + // if the WcSdk `onSessionSettleResponse` callback arrives late, merge pending approvals with WcSdk sessions + val savedPending = store.pendingApproval.first() + .mapNotNullTo(mutableSetOf()) { savedPendingSession -> + val sdkSession = inSdk + .find { sdkSession -> sdkSession.pairingTopic == savedPendingSession.pairingTopic } + ?: return@mapNotNullTo null + savedPendingSession.session.copy(topic = sdkSession.topic) + } + + val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession -> val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null val networks = wcNetworksConverter.findWalletNetworks(wallet, sdkSession) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcScope.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcScope.kt new file mode 100644 index 0000000000..d394ce320d --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcScope.kt @@ -0,0 +1,13 @@ +package com.tangem.data.walletconnect.utils + +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +internal class WcScope( + dispatchers: CoroutineDispatcherProvider, +) : CoroutineScope { + + override val coroutineContext: CoroutineContext = SupervisorJob() + dispatchers.io +} \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 6058a8f797..60a4fe2b99 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -16,11 +16,7 @@ import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletconnect.model.WcPairError -import com.tangem.domain.walletconnect.model.WcPairRequest -import com.tangem.domain.walletconnect.model.WcSession -import com.tangem.domain.walletconnect.model.WcSessionApprove -import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.usecase.pair.WcPairState import io.mockk.coEvery import io.mockk.coVerifyOrder @@ -32,7 +28,6 @@ import org.junit.Test internal class DefaultWcPairUseCaseTest { - private val sessionsManager: WcSessionsManager = mockk() private val associateNetworksDelegate: AssociateNetworksDelegate = mockk() private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk() private val analytics: AnalyticsEventHandler = mockk(relaxed = true) @@ -83,11 +78,6 @@ internal class DefaultWcPairUseCaseTest { namespaces = mapOf(), ) - private val sdkApproveSuccess: Wallet.Model.SettledSessionResponse.Result - get() = Wallet.Model.SettledSessionResponse.Result( - session = sdkSession, - ) - private val sdkSession: Wallet.Model.Session get() = Wallet.Model.Session( pairingTopic = "", @@ -115,7 +105,6 @@ internal class DefaultWcPairUseCaseTest { ) private fun useCaseFactory() = DefaultWcPairUseCase( - sessionsManager = sessionsManager, associateNetworksDelegate = associateNetworksDelegate, caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, @@ -155,12 +144,11 @@ internal class DefaultWcPairUseCaseTest { @Test fun `success pair and approve flow`() = runTest { val approveLoading = WcPairState.Approving.Loading(sessionForApprove) - val sessionForSave = sdkSession.sessionForSave - val result = WcPairState.Approving.Result(sessionForApprove, sessionForSave.right()) + val appMetaData = sdkSession.sessionForSave.sdkModel.appMetaData + val result = WcPairState.Approving.Result(sessionForApprove, appMetaData.right()) coEvery { sdkDelegate.pair(url) } returns (sdkProposal to sdkVerifyContext).right() - coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right() - coEvery { sessionsManager.saveSession(any()) } returns Unit + coEvery { sdkDelegate.approve(any(), any()) } returns Unit.right() coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } val useCase = useCaseFactory() @@ -177,8 +165,7 @@ internal class DefaultWcPairUseCaseTest { assertEquals(approveLoading, awaitItem()) coVerifyOrder { - sdkDelegate.approve(sdkApprove) - sessionsManager.saveSession(any()) + sdkDelegate.approve(any(), any()) } val actual: WcPairState = awaitItem() assert(actual is WcPairState.Approving.Result) @@ -250,7 +237,7 @@ internal class DefaultWcPairUseCaseTest { val approveLoading = WcPairState.Approving.Loading(sessionForApprove) val error = WcPairError.ApprovalFailed("error").left() coEvery { sdkDelegate.pair(url) } returns (sdkProposal to sdkVerifyContext).right() - coEvery { sdkDelegate.approve(sdkApprove) } returns error + coEvery { sdkDelegate.approve(any(), any()) } returns error coEvery { sdkDelegate.rejectSession(sdkApprove.proposerPublicKey) } returns Unit coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } @@ -269,7 +256,7 @@ internal class DefaultWcPairUseCaseTest { assertEquals(approveLoading, awaitItem()) coVerifyOrder { - sdkDelegate.approve(sdkApprove) + sdkDelegate.approve(any(), any()) sdkDelegate.rejectSession(sdkApprove.proposerPublicKey) } assertEquals(errorResult, awaitItem()) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt index 572d3aeb9a..15ccc11fc1 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -11,4 +11,15 @@ data class WcSessionDTO( val url: String?, val securityStatus: CheckDAppResult = CheckDAppResult.FAILED_TO_VERIFY, val connectingTime: Long? = null, +) + +/** + * keep in mind that [WcSessionDTO.topic] will be empty + * you must associate it with SdkSession by [pairingTopic] + */ +@JsonClass(generateAdapter = true) +data class WcPendingApprovalSessionDTO( + val pairingTopic: String, + val session: WcSessionDTO, + val expiredTime: Long, ) \ No newline at end of file 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 c2c2af4914..d74c2e9960 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 @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.Flow interface WcSessionsManager { val sessions: Flow>> - suspend fun saveSession(session: WcSession) suspend fun removeSession(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/method/BlockAidTransactionCheck.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/BlockAidTransactionCheck.kt index 925877b063..4447132e96 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/BlockAidTransactionCheck.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/BlockAidTransactionCheck.kt @@ -1,7 +1,6 @@ package com.tangem.domain.walletconnect.usecase.method import com.domain.blockaid.models.transaction.CheckTransactionResult -import com.domain.blockaid.models.transaction.simultation.TokenInfo import com.tangem.domain.core.lce.LceFlow interface BlockAidTransactionCheck { @@ -13,14 +12,6 @@ interface BlockAidTransactionCheck { data class Plain(override val result: CheckTransactionResult) : Result - data class Approval( - override val result: CheckTransactionResult, - val approval: WcApproval, - val tokenInfo: TokenInfo, - val isMutable: Boolean, - ) : Result { - - suspend fun approvalAmount() = approval.getAmount() - } + data class Approval(override val result: CheckTransactionResult) : Result } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt index 1097cfc7a5..a306af513f 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.walletconnect.usecase.pair import arrow.core.Either import com.tangem.domain.walletconnect.model.* +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import kotlinx.coroutines.flow.Flow interface WcPairUseCase { @@ -27,7 +28,7 @@ sealed interface WcPairState { data class Loading(override val session: WcSessionApprove) : Approving data class Result( override val session: WcSessionApprove, - val result: Either, + val result: Either, ) : Approving } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 7449212ae2..bf416fe8f2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.* +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -96,7 +97,8 @@ internal class WcPairModel @Inject constructor( appInfoUiState.transformerUpdate( WcConnectButtonProgressTransformer(showProgress = false), ) - pairState.result + pairState + .result .onLeft(::processError) .onRight(::processSuccessfullyConnected) router.pop() @@ -203,12 +205,12 @@ internal class WcPairModel @Inject constructor( stackNavigation.pushNew(WcAppInfoRoutes.Alert.Verified(appName)) } - private fun processSuccessfullyConnected(session: WcSession) { + private fun processSuccessfullyConnected(session: WcAppMetaData) { messageSender.send( SnackbarMessage( message = resourceReference( id = R.string.wc_connected_to, - formatArgs = wrappedList(session.sdkModel.appMetaData.name), + formatArgs = wrappedList(session.name), ), ), )