Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-09 13:24:16 +07:00
parent 72a3eb6d7f
commit c21b3f1231
11 changed files with 390 additions and 148 deletions

View file

@ -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)

View file

@ -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(

View file

@ -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<WcSdkObserver>(
sessionsManager,
networkService,
wcPairFlow,
pairSdkDelegate,
)
override fun init(projectId: String) {

View file

@ -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<UserWalletId, ProposalNetwork> {
suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map<UserWallet, ProposalNetwork> {
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
val requiredNamespaces: Set<CAIP2> = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces: Set<CAIP2> = 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<CryptoCurrency.Coin>()
.map { it.network }
val unknownRequired = mutableSetOf<WcNetwork.Unknown>()
val missingRequired = mutableSetOf<WcNetwork.Supported>()
val required = mutableSetOf<WcNetwork.Supported>()
val available = mutableSetOf<WcNetwork.Supported>()
val notAdded = mutableSetOf<WcNetwork.Supported>()
val unknownRequired = mutableSetOf<String>()
val missingRequired = mutableSetOf<Network>()
val required = mutableSetOf<Network>()
val available = mutableSetOf<Network>()
val notAdded = mutableSetOf<Network>()
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,

View file

@ -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<TerminalAction>()
private val onSessionProposal =
Channel<Pair<Wallet.Model.SessionProposal, Wallet.Model.VerifyContext>>(Channel.BUFFERED)
private val onSessionSettleResponse = Channel<Wallet.Model.SettledSessionResponse>(Channel.BUFFERED)
override fun pairFlow(uri: String, source: WcPairUseCase.Source): Flow<WcPairState> {
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<Throwable, Unit> =
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<Throwable, Unit> {
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> {
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(

View file

@ -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<Wallet.Model.SessionProposal>()
private val onSessionSettleResponse = Channel<Wallet.Model.SettledSessionResponse>()
suspend fun pair(url: String): Either<WcPairError, Wallet.Model.SessionProposal> = 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<WcPairError, Wallet.Model.SettledSessionResponse.Result> = 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<WcPairError, Unit> {
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<WcPairError, Unit> {
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()
}

View file

@ -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<WcSessionsManager>()
private val associateNetworksDelegate: AssociateNetworksDelegate = mockk<AssociateNetworksDelegate>()
private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk<CaipNamespaceDelegate>()
private val sdkDelegate: WcPairSdkDelegate = mockk<WcPairSdkDelegate>()
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()
}
}
}

View file

@ -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
}

View file

@ -4,7 +4,7 @@ sealed class WcPairError(override val message: String) : Exception(message) {
data object UnsupportedDApp : WcPairError("UnsupportedDApp")
data class UnsupportedNetworks(
val chains: Set<WcNetwork.Unknown>,
val chains: Set<String>,
) : WcPairError("ApprovalErrorMissingNetworks")
data class ExternalApprovalError(

View file

@ -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<WcNetwork.Supported>,
val network: List<Network>,
)

View file

@ -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<UserWalletId, ProposalNetwork>,
val proposalNetwork: Map<UserWallet, ProposalNetwork>,
val securityStatus: Any,
) {
data class ProposalNetwork(
val walletId: UserWalletId,
val missingRequired: Set<WcNetwork.Supported>,
val required: Set<WcNetwork.Supported>,
val available: Set<WcNetwork.Supported>,
val notAdded: Set<WcNetwork.Supported>,
val wallet: UserWallet,
val missingRequired: Set<Network>,
val required: Set<Network>,
val available: Set<Network>,
val notAdded: Set<Network>,
)
}