Updated on 2026-08-14
This commit is contained in:
parent
72a3eb6d7f
commit
c21b3f1231
11 changed files with 390 additions and 148 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue