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 1e6e533e5d..b35c96e84d 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 @@ -48,7 +48,7 @@ internal class DefaultWcInitializeUseCase( application = application, metaData = appMetaData, ) { error -> - Timber.e("Error while initializing client: $error") + Timber.tag(WC_TAG).e("Error while initializing client: $error") } WalletKit.initialize( @@ -60,7 +60,7 @@ internal class DefaultWcInitializeUseCase( Timber.tag(WC_TAG).i("onWcSdkInit") }, onError = { error -> - Timber.e("Error while initializing Web3Wallet: $error") + Timber.tag(WC_TAG).e("Error while initializing Web3Wallet: $error") }, ) } 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 808d399dab..3e01f6c9c3 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 @@ -2,6 +2,7 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet.Model.Namespace +import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -33,6 +34,8 @@ internal class AssociateNetworksDelegate( val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() + // remove duplicates + .subtract(requiredNamespaces) return userWallets .associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) } @@ -58,7 +61,8 @@ internal class AssociateNetworksDelegate( return@forEach } val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } - if (walletNetwork == null) { + + if (walletNetwork == null || isCustomCoin(walletNetwork)) { missingRequired.add(wcNetwork) } else { required.add(walletNetwork) @@ -68,7 +72,7 @@ internal class AssociateNetworksDelegate( val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@forEach val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } - if (walletNetwork != null) { + if (walletNetwork != null && !isCustomCoin(walletNetwork)) { available.add(walletNetwork) } else { notAdded.add(wcNetwork) 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 6b60b36eca..784290a509 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 @@ -56,7 +56,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") - val error = WcPairState.Error(WcPairError.UnsupportedDomain) + val error = WcPairState.Error(WcPairError.UnsupportedDApp) emit(error) return@flow } 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 0b83809241..52df62becd 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 @@ -5,59 +5,94 @@ 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.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.domain.walletconnect.model.WcPairError -import kotlinx.coroutines.async +import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeout +import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds internal class WcPairSdkDelegate : WcSdkObserver { private val onSessionProposal = Channel() + private val onSdkErrorCallback = Channel() private val onSessionSettleResponse = Channel() suspend fun pair(url: String): Either = coroutineScope { - suspend fun proposalCallback() = onSessionProposal - .receiveAsFlow() - .first() - + val proposalCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { proposalCallback() } } val pairCall = async { sdkPair(url) } - val proposal = async { withTimeout(20.seconds) { proposalCallback() } } pairCall.await().onLeft { - proposal.cancel() + proposalCallback.cancel() return@coroutineScope it.left() } - proposal.await().right() + proposalCallback.await() } + private suspend fun proposalCallback() = callbackFlow { + // wait first onSessionProposal callback + launch { + val sessionProposal = onSessionProposal.receiveAsFlow().first() + trySend(sessionProposal.right()) + channel.close() + } + // OR + // wait first onError callback + launch { + val error = onSdkErrorCallback.receiveAsFlow().first() + trySend(error.throwable.toPairError().left()) + channel.close() + } + awaitClose() + }.first() + suspend fun approve( sessionApprove: Wallet.Params.SessionApprove, ): Either = coroutineScope { - suspend fun approveCallback() = onSessionSettleResponse - .receiveAsFlow() - .first() - + val approveCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { approveCallback() } } val approveCall = async { sdkApprove(sessionApprove) } - val approveCallback = async { withTimeout(20.seconds) { approveCallback() } } approveCall.await() .onLeft { approveCallback.cancel() return@coroutineScope it.left() } - when (val result = approveCallback.await()) { - is Wallet.Model.SettledSessionResponse.Result -> result.right() - is Wallet.Model.SettledSessionResponse.Error -> - WcPairError.ApprovalFailed(result.errorMessage).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() + } + }, + ) } + 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( params = Wallet.Params.SessionReject( proposerPublicKey = proposerPublicKey, @@ -68,6 +103,10 @@ internal class WcPairSdkDelegate : WcSdkObserver { ) } + override fun onError(error: Wallet.Model.Error) { + onSdkErrorCallback.trySend(error) + } + override fun onSessionProposal( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, @@ -85,8 +124,8 @@ internal class WcPairSdkDelegate : WcSdkObserver { return suspendCancellableCoroutine { continuation -> WalletKit.approveSession( params = sessionApprove, - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toApproveError()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.toApproveError()) }, ) } } @@ -95,8 +134,8 @@ internal class WcPairSdkDelegate : WcSdkObserver { return suspendCancellableCoroutine { continuation -> WalletKit.pair( params = Wallet.Params.Pair(uri), - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toPairError().left()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.toPairError().left()) }, ) } } @@ -109,7 +148,12 @@ internal class WcPairSdkDelegate : WcSdkObserver { private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left() companion object { + private const val CALLBACK_TIMEOUT = 30 // com.reown.android.pairing.engine.domain.PairingEngine.pair - private val pairingExpiredMessages = listOf("Pairing URI expired", "Pairing expired") + private val pairingExpiredMessages = listOf( + "Pairing URI expired", + "Pairing expired", + "No proposal or pending session authenticate request for pairing topic", + ) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index 57961795e5..e87d12d41f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -24,10 +24,12 @@ internal class DefaultWcRespondService : WcRespondService { ), ), onSuccess = { + if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).i("Successful respond for request $request") continuation.resume(Unit.right()) }, onError = { + if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") continuation.resume(it.throwable.left()) }, 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 66211e5d83..b82c70ec75 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 @@ -35,7 +35,6 @@ internal class DefaultWcSessionsManager( private val onSessionDelete = Channel(capacity = Channel.BUFFERED) private val oneTimeMigration = MutableStateFlow(true) - private val oneTimeSessionExtend = MutableStateFlow(true) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } @@ -50,10 +49,6 @@ internal class DefaultWcSessionsManager( val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one - if (oneTimeSessionExtend.value) { - oneTimeSessionExtend.value = false - scope.launch { extendAliveSessions(associatedSessions) } - } emit(associatedSessions.groupBy { it.wallet }) } .distinctUntilChanged() @@ -61,8 +56,8 @@ internal class DefaultWcSessionsManager( override fun onWcSdkInit() { oneTimeMigration.value = true - oneTimeSessionExtend.value = true listenOnSessionDelete() + extendSessions() } override suspend fun saveSession(session: WcSession) { @@ -154,17 +149,20 @@ internal class DefaultWcSessionsManager( return haveSomeUnknown } - private suspend fun extendAliveSessions(sessions: List) = coroutineScope { - val jobs = sessions.map { launch { sdkSessionExtend(it.sdkModel.topic) } } - jobs.joinAll() + private fun extendSessions() { + scope.launch(dispatchers.io) { + val topics: List = WalletKit.getListOfActiveSessions().map { it.topic } + val jobs = topics.map { topic -> launch { sdkSessionExtend(topic) } } + jobs.joinAll() + } } private suspend fun sdkDisconnectSession(topic: String): Either { return suspendCancellableCoroutine { continuation -> WalletKit.disconnectSession( params = Wallet.Params.SessionDisconnect(topic), - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.left()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) }, ) } } @@ -173,8 +171,8 @@ internal class DefaultWcSessionsManager( return suspendCancellableCoroutine { continuation -> WalletKit.extendSession( params = Wallet.Params.SessionExtend(topic), - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.left()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) }, ) } } 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 47778833c3..fecea507d0 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 @@ -196,7 +196,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `success pair and reject unsupported dApp`() = runTest { coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() - val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDomain) + val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) val useCase = useCaseFactory() useCase.invoke().test { 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 005daafedc..7d896a8539 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 @@ -8,7 +8,7 @@ sealed class WcPairError( data class UriAlreadyUsed(override val message: String) : WcPairError("107 001 001") data class PairingFailed(override val message: String) : WcPairError("107 001 002") data object InvalidDomainURL : WcPairError("107 001 003") - data object UnsupportedDomain : WcPairError("107 001 004") + data object UnsupportedDApp : WcPairError("107 001 004") data class UnsupportedBlockchains(val chains: Set) : WcPairError("107 001 005") data object InvalidConnectionRequest : WcPairError("107 002 001")