From c21b3f1231db4a5bf2990923a581bdd3d77721fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 13:24:16 +0700 Subject: [PATCH] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 1 + .../di/WalletConnectDataModule.kt | 11 +- .../initialize/DefaultWcInitializeUseCase.kt | 6 +- .../pair/AssociateNetworksDelegate.kt | 34 ++- .../pair/DefaultWcPairUseCase.kt | 122 ++------- .../walletconnect/pair/WcPairSdkDelegate.kt | 98 ++++++++ .../walletconnect/DefaultWcPairUseCaseTest.kt | 232 ++++++++++++++++++ .../domain/walletconnect/model/WcNetwork.kt | 14 -- .../domain/walletconnect/model/WcPairError.kt | 2 +- .../walletconnect/model/WcSessionApprove.kt | 3 +- .../walletconnect/model/WcSessionProposal.kt | 15 +- 11 files changed, 390 insertions(+), 148 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt create mode 100644 data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt delete mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 0aa064eccb..59061799bf 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(deps.arrow.core) /* Tests */ + testImplementation(projects.common.test) testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) testImplementation(deps.test.mockk) 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 ee70b52018..8c735d8bad 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 @@ -11,6 +11,7 @@ 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.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.request.WcMethodHandler import com.tangem.data.walletconnect.respond.DefaultWcRespondService @@ -59,12 +60,12 @@ internal object WalletConnectDataModule { application: Application, sessionsManager: DefaultWcSessionsManager, networkService: DefaultWcRequestService, - wcPairFlow: DefaultWcPairUseCase, + pairSdkDelegate: WcPairSdkDelegate, ): WcInitializeUseCase = DefaultWcInitializeUseCase( application = application, sessionsManager = sessionsManager, networkService = networkService, - wcPairFlow = wcPairFlow, + pairSdkDelegate = pairSdkDelegate, ) @Provides @@ -73,16 +74,22 @@ internal object WalletConnectDataModule { sessionsManager: WcSessionsManager, associateNetworksDelegate: AssociateNetworksDelegate, caipNamespaceDelegate: CaipNamespaceDelegate, + sdkDelegate: WcPairSdkDelegate, ): DefaultWcPairUseCase = DefaultWcPairUseCase( sessionsManager = sessionsManager, associateNetworksDelegate = associateNetworksDelegate, caipNamespaceDelegate = caipNamespaceDelegate, + sdkDelegate = sdkDelegate, ) @Provides @Singleton fun wcPairUseCase(default: DefaultWcPairUseCase): WcPairUseCase = default + @Provides + @Singleton + fun sdkDelegate(): WcPairSdkDelegate = WcPairSdkDelegate() + @Provides @Singleton fun defaultWcSessionsManager( 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 6330859a84..91db31c6c8 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 @@ -6,7 +6,7 @@ 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.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcSdkObserver @@ -17,13 +17,13 @@ internal class DefaultWcInitializeUseCase( private val application: Application, private val sessionsManager: DefaultWcSessionsManager, private val networkService: DefaultWcRequestService, - private val wcPairFlow: DefaultWcPairUseCase, + private val pairSdkDelegate: WcPairSdkDelegate, ) : WcInitializeUseCase { private val wcSdkObservers = mutableSetOf( sessionsManager, networkService, - wcPairFlow, + pairSdkDelegate, ) override fun init(projectId: String) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index e24b2641ac..ce3ec70378 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -8,12 +8,11 @@ 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.model.Network 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( @@ -24,14 +23,13 @@ internal class AssociateNetworksDelegate constructor( ) { @Throws(WcPairError.UnsupportedNetworks::class) - suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { + suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() - return userWallets.associate { wallet -> - wallet.walletId to mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) - } + return userWallets + .associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) } } private suspend fun mapNetworksForWallet( @@ -43,11 +41,11 @@ internal class AssociateNetworksDelegate constructor( .filterIsInstance() .map { it.network } - val unknownRequired = mutableSetOf() - val missingRequired = mutableSetOf() - val required = mutableSetOf() - val available = mutableSetOf() - val notAdded = mutableSetOf() + val unknownRequired = mutableSetOf() + val missingRequired = mutableSetOf() + val required = mutableSetOf() + val available = mutableSetOf() + val notAdded = mutableSetOf() fun CAIP2.toBlockchain() = namespaceConverters[NamespaceKey(this.namespace)]?.toBlockchain(this) fun Blockchain.toNetwork() = getNetwork( @@ -60,33 +58,33 @@ internal class AssociateNetworksDelegate constructor( requiredNamespaces.forEach { chainId -> val blockchain = chainId.toBlockchain() if (blockchain == null) { - unknownRequired.add(WcNetwork.Unknown(missingNetworkName(chainId))) + unknownRequired.add(missingNetworkName(chainId)) return@forEach } val wcNetwork = blockchain.toNetwork() if (wcNetwork == null) { - unknownRequired.add(WcNetwork.Unknown(missingNetworkName(blockchain))) + unknownRequired.add(missingNetworkName(blockchain)) return@forEach } val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } if (walletNetwork == null) { - missingRequired.add(WcNetwork.Supported(wcNetwork)) + missingRequired.add(wcNetwork) } else { - required.add(WcNetwork.Supported(walletNetwork)) + required.add(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)) + available.add(wcNetwork) } else { - notAdded.add(WcNetwork.Supported(walletNetwork)) + notAdded.add(walletNetwork) } } if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedNetworks(unknownRequired) return ProposalNetwork( - walletId = wallet.walletId, + wallet = wallet, missingRequired = missingRequired, required = required, available = available, 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 98dcdf4de2..28776d97c2 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 @@ -4,8 +4,6 @@ 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 import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession @@ -21,9 +19,7 @@ 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 val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") @@ -31,27 +27,18 @@ internal class DefaultWcPairUseCase( private val sessionsManager: WcSessionsManager, private val associateNetworksDelegate: AssociateNetworksDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate, -) : WcPairUseCase, WcSdkObserver { + private val sdkDelegate: WcPairSdkDelegate, +) : WcPairUseCase { private val onCallTerminalAction = Channel() - private val onSessionProposal = - Channel>(Channel.BUFFERED) - private val onSessionSettleResponse = Channel(Channel.BUFFERED) override fun pairFlow(uri: String, source: WcPairUseCase.Source): Flow { return flow { emit(WcPairState.Loading) - // call sdk.pair and wait result, finish flow on error - walletKitPair(uri).onLeft { throwable -> - emit(WcPairState.Error(WcPairError.Unknown(throwable.localizedMessage.orEmpty()))) - return@flow - } - // wait for sdk onSessionProposal callback - val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow() - .first { (sessionProposal, verifyContext) -> - true // todo(wc) check verifyContext? compare uri? - } + val sdkSessionProposal = sdkDelegate.pair(uri) + .onLeft { emit(WcPairState.Error(it)) } + .getOrNull() ?: return@flow // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { @@ -62,44 +49,32 @@ internal class DefaultWcPairUseCase( } val proposalState = buildProposalState(sdkSessionProposal) - .fold(ifLeft = { WcPairState.Error(it) }, ifRight = { it }) + .onLeft { emit(WcPairState.Error(it)) } + .getOrNull() ?: return@flow emit(proposalState) // wait first terminal action and continue WC pair flow val terminalAction = onCallTerminalAction.receiveAsFlow().first() val sessionForApprove: WcSessionApprove? = when (terminalAction) { is TerminalAction.Approve -> terminalAction.sessionForApprove - TerminalAction.Reject -> { - // non suspending WalletKit.rejectSession call - rejectSession(sdkSessionProposal.proposerPublicKey) - null - } + TerminalAction.Reject -> null } // finish flow if rejected above - sessionForApprove ?: return@flow + if (sessionForApprove == null) { + sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) + return@flow + } // start flow of approving in wc sdk emit(WcPairState.Approving.Loading(sessionForApprove)) - // call sdk approve and wait result 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.saveSession(newSession) - newSession.right() - } - } - }, - ) + ).map { settledSession -> + val newSession = settledSession.session.toDomain(sessionForApprove.wallet) + sessionsManager.saveSession(newSession) + newSession + } emit(WcPairState.Approving.Result(sessionForApprove, either)) } } @@ -112,77 +87,20 @@ internal class DefaultWcPairUseCase( onCallTerminalAction.trySend(TerminalAction.Reject) } - override fun onSessionProposal( - sessionProposal: Wallet.Model.SessionProposal, - verifyContext: Wallet.Model.VerifyContext, - ) { - // Triggered when wallet receives the session proposal sent by a Dapp - Timber.i("sessionProposal: $sessionProposal") - onSessionProposal.trySend(sessionProposal to verifyContext) - } - - override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { - // Triggered when wallet receives the session settlement response from Dapp - Timber.i("onSessionSettleResponse: $settleSessionResponse") - onSessionSettleResponse.trySend(settleSessionResponse) - } - - private suspend fun walletKitPair(uri: String): Either = - suspendCancellableCoroutine { continuation -> - WalletKit.pair( - params = Wallet.Params.Pair(uri), - onSuccess = { - Timber.i("Paired successfully: $it") - continuation.resume(Unit.right()) - }, - onError = { - Timber.e("Error while pairing: $it") - continuation.resume(it.throwable.left()) - }, - ) - } - private suspend fun walletKitApproveSession( sessionForApprove: WcSessionApprove, sdkSessionProposal: Wallet.Model.SessionProposal, - ): Either { + ): Either { val namespaces = caipNamespaceDelegate.associate( sdkSessionProposal, sessionForApprove.wallet, - sessionForApprove.network.map { it.network }, + sessionForApprove.network, ) val sessionApprove = Wallet.Params.SessionApprove( proposerPublicKey = sdkSessionProposal.proposerPublicKey, namespaces = namespaces, ) - return suspendCancellableCoroutine { continuation -> - WalletKit.approveSession( - params = sessionApprove, - onSuccess = { - Timber.i("Approved successfully: $it") - continuation.resume(Unit.right()) - }, - onError = { - Timber.e("Error while approving: $it") - continuation.resume(it.throwable.left()) - }, - ) - } - } - - private fun rejectSession(proposerPublicKey: String) { - WalletKit.rejectSession( - params = Wallet.Params.SessionReject( - proposerPublicKey = proposerPublicKey, - reason = "", - ), - onSuccess = { - Timber.i("Rejected successfully: $it") - }, - onError = { - Timber.e("Error while rejecting: $it") - }, - ) + return sdkDelegate.approve(sessionApprove) } private suspend fun buildProposalState( 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 new file mode 100644 index 0000000000..423eeb9ea3 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -0,0 +1,98 @@ +package com.tangem.data.walletconnect.pair + +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 +import com.tangem.domain.walletconnect.model.WcPairError +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +class WcPairSdkDelegate : WcSdkObserver { + + private val onSessionProposal = Channel() + private val onSessionSettleResponse = Channel() + + suspend fun pair(url: String): Either = coroutineScope { + suspend fun proposalCallback() = onSessionProposal + .receiveAsFlow() + .filter { proposal -> proposal.url == url } + .first() + + val pairCall = async { sdkPair(url) } + val proposal = async { proposalCallback() } + pairCall.await().map { proposal.await() } + } + + suspend fun approve( + sessionApprove: Wallet.Params.SessionApprove, + ): Either = coroutineScope { + suspend fun approveCallback() = onSessionSettleResponse + .receiveAsFlow() + .first() + + val approveCall = async { sdkApprove(sessionApprove) } + val approveCallback = async { approveCallback() } + approveCall.await() + .onLeft { return@coroutineScope it.left() } + when (val result = approveCallback.await()) { + is Wallet.Model.SettledSessionResponse.Result -> result.right() + is Wallet.Model.SettledSessionResponse.Error -> + WcPairError.ExternalApprovalError(result.errorMessage).left() + } + } + + fun rejectSession(proposerPublicKey: String) { + WalletKit.rejectSession( + params = Wallet.Params.SessionReject( + proposerPublicKey = proposerPublicKey, + reason = "", + ), + onSuccess = {}, + onError = {}, + ) + } + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + // Triggered when wallet receives the session proposal sent by a Dapp + onSessionProposal.trySend(sessionProposal) + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + // Triggered when wallet receives the session settlement response from Dapp + onSessionSettleResponse.trySend(settleSessionResponse) + } + + private suspend fun sdkApprove(sessionApprove: Wallet.Params.SessionApprove): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.approveSession( + params = sessionApprove, + onSuccess = { continuation.resume(Unit.right()) }, + onError = { continuation.resume(it.throwable.toPairError()) }, + ) + } + } + + private suspend fun sdkPair(uri: String): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.pair( + params = Wallet.Params.Pair(uri), + onSuccess = { continuation.resume(Unit.right()) }, + onError = { continuation.resume(it.throwable.toPairError()) }, + ) + } + } + + private fun Throwable.toPairError() = WcPairError.ExternalApprovalError(this.localizedMessage.orEmpty()).left() +} \ 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 new file mode 100644 index 0000000000..86ed85e03e --- /dev/null +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -0,0 +1,232 @@ +package com.tangem.domain.walletconnect + +import app.cash.turbine.test +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +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.pair.WcPairSdkDelegate +import com.tangem.data.walletconnect.utils.WcSdkSessionConverter +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.repository.WcSessionsManager +import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +internal class DefaultWcPairUseCaseTest { + + private val sessionsManager: WcSessionsManager = mockk() + private val associateNetworksDelegate: AssociateNetworksDelegate = mockk() + private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk() + private val sdkDelegate: WcPairSdkDelegate = mockk() + + private val url = "testUrl" + private val source = WcPairUseCase.Source.QR + private val loading = WcPairState.Loading + private val sdkProposal: Wallet.Model.SessionProposal + get() = Wallet.Model.SessionProposal( + pairingTopic = "", + name = "", + description = "", + url = "", + icons = listOf(), + redirect = "", + requiredNamespaces = mapOf(), + optionalNamespaces = mapOf(), + properties = mapOf(), + proposerPublicKey = "", + relayProtocol = "", + relayData = "", + ) + + private val unsupportedDApp = "Apex Pro" + private val unsupportedSdkProposal get() = sdkProposal.copy(name = unsupportedDApp) + + private val sessionForApprove: WcSessionApprove + get() = WcSessionApprove( + wallet = MockUserWalletFactory.create(), + network = listOf(), + ) + + private val sdkApprove: Wallet.Params.SessionApprove + get() = Wallet.Params.SessionApprove( + proposerPublicKey = "", + 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 = "", + topic = "", + expiry = 0L, + requiredNamespaces = mapOf(), + optionalNamespaces = mapOf(), + namespaces = mapOf(), + metaData = null, + ) + + private val Wallet.Model.Session.sessionForSave: WcSession + get() = WcSession( + wallet = sessionForApprove.wallet, + sdkModel = WcSdkSessionConverter.convert(this), + ) + + private val useCase = DefaultWcPairUseCase( + sessionsManager = sessionsManager, + associateNetworksDelegate = associateNetworksDelegate, + caipNamespaceDelegate = caipNamespaceDelegate, + sdkDelegate = sdkDelegate, + ) + + @Before + fun setup() { + coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf() + coEvery { + caipNamespaceDelegate.associate( + sessionProposal = sdkProposal, + userWallet = sessionForApprove.wallet, + networks = sessionForApprove.network, + ) + } returns mapOf() + } + + @Test + fun `pair, emmit proposal state and wait actions`() = runTest { + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assert(awaitItem() is WcPairState.Proposal) + expectNoEvents() + } + } + + @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()) + + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right() + coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assert(awaitItem() is WcPairState.Proposal) + useCase.approve(sessionForApprove) + // ignore + useCase.reject() + + assertEquals(approveLoading, awaitItem()) + coVerifyOrder { + sdkDelegate.approve(sdkApprove) + sessionsManager.saveSession(sessionForSave) + } + assertEquals(result, awaitItem()) + awaitComplete() + } + } + + @Test + fun `success pair and reject approving`() = runTest { + val proposerPublicKey = sdkProposal.proposerPublicKey + + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { sdkDelegate.rejectSession(proposerPublicKey) } returns Unit + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assert(awaitItem() is WcPairState.Proposal) + useCase.reject() + coVerifyOrder { + sdkDelegate.rejectSession(sdkProposal.proposerPublicKey) + } + awaitComplete() + } + } + + @Test + fun `success pair and reject unsupported dApp`() = runTest { + coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() + val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assertEquals(unsupportedDAppError, awaitItem()) + awaitComplete() + } + } + + @Test + fun `complete on pair error`() = runTest { + val error = WcPairError.ExternalApprovalError("error") + coEvery { sdkDelegate.pair(url) } returns error.left() + val errorState = WcPairState.Error(error) + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assertEquals(errorState, awaitItem()) + awaitComplete() + } + } + + @Test + fun `complete on approve error`() = runTest { + val approveLoading = WcPairState.Approving.Loading(sessionForApprove) + val error = WcPairError.ExternalApprovalError("error").left() + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { sdkDelegate.approve(sdkApprove) } returns error + + val errorResult = WcPairState.Approving.Result(sessionForApprove, error) + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + associateNetworksDelegate.associate(sdkProposal) + } + assert(awaitItem() is WcPairState.Proposal) + useCase.approve(sessionForApprove) + + assertEquals(approveLoading, awaitItem()) + coVerifyOrder { + sdkDelegate.approve(sdkApprove) + } + + assertEquals(errorResult, awaitItem()) + awaitComplete() + } + } +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt deleted file mode 100644 index 1ef5352c31..0000000000 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.walletconnect.model - -import com.tangem.domain.tokens.model.Network - -sealed interface WcNetwork { - - val name: String - - data class Supported(val network: Network) : WcNetwork { - override val name: String get() = network.name - } - - data class Unknown(override val name: String) : WcNetwork -} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt index 8f90d64512..5a060eaaba 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -4,7 +4,7 @@ sealed class WcPairError(override val message: String) : Exception(message) { data object UnsupportedDApp : WcPairError("UnsupportedDApp") data class UnsupportedNetworks( - val chains: Set, + val chains: Set, ) : WcPairError("ApprovalErrorMissingNetworks") data class ExternalApprovalError( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt index a8f7d6b70a..427c4a9d89 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt @@ -1,8 +1,9 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet data class WcSessionApprove( val wallet: UserWallet, - val network: List, + val network: List, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt index 648163817b..50d299303f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -1,19 +1,20 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet data class WcSessionProposal( val dAppMetaData: WcAppMetaData, - val proposalNetwork: Map, + val proposalNetwork: Map, val securityStatus: Any, ) { data class ProposalNetwork( - val walletId: UserWalletId, - val missingRequired: Set, - val required: Set, - val available: Set, - val notAdded: Set, + val wallet: UserWallet, + val missingRequired: Set, + val required: Set, + val available: Set, + val notAdded: Set, ) } \ No newline at end of file