Updated on 2026-08-14
This commit is contained in:
parent
3dbaff9f06
commit
776d00eb57
8 changed files with 94 additions and 46 deletions
|
|
@ -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")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> = sessionProposal.requiredNamespaces.setOfChainId()
|
||||
val optionalNamespaces: Set<String> = 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Wallet.Model.SessionProposal>()
|
||||
private val onSdkErrorCallback = Channel<Wallet.Model.Error>()
|
||||
private val onSessionSettleResponse = Channel<Wallet.Model.SettledSessionResponse>()
|
||||
|
||||
suspend fun pair(url: String): Either<WcPairError, Wallet.Model.SessionProposal> = 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<WcPairError, Wallet.Model.SettledSessionResponse.Result> = 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<Either<WcPairError, Wallet.Model.SettledSessionResponse>> {
|
||||
// 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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ internal class DefaultWcSessionsManager(
|
|||
|
||||
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
|
||||
private val oneTimeMigration = MutableStateFlow(true)
|
||||
private val oneTimeSessionExtend = MutableStateFlow(true)
|
||||
|
||||
override val sessions: Flow<Map<UserWallet, List<WcSession>>>
|
||||
get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore }
|
||||
|
|
@ -50,10 +49,6 @@ internal class DefaultWcSessionsManager(
|
|||
val associatedSessions: List<WcSession> = 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<WcSession>) = coroutineScope {
|
||||
val jobs = sessions.map { launch { sdkSessionExtend(it.sdkModel.topic) } }
|
||||
jobs.joinAll()
|
||||
private fun extendSessions() {
|
||||
scope.launch(dispatchers.io) {
|
||||
val topics: List<String> = WalletKit.getListOfActiveSessions().map { it.topic }
|
||||
val jobs = topics.map { topic -> launch { sdkSessionExtend(topic) } }
|
||||
jobs.joinAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sdkDisconnectSession(topic: String): Either<Throwable, Unit> {
|
||||
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()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<String>) : WcPairError("107 001 005")
|
||||
|
||||
data object InvalidConnectionRequest : WcPairError("107 002 001")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue