From 4c08ee6790e322081f39a566b041baebd353c21d Mon Sep 17 00:00:00 2001 From: kubel Date: Mon, 26 Aug 2024 13:33:54 +0200 Subject: [PATCH 1/7] Connect when intention to use Relay --- .../connection/DefaultConnectionLifecycle.kt | 82 ++++++ .../internal/common/di/AndroidCommonDITags.kt | 2 +- .../internal/common/di/CoreJsonRpcModule.kt | 1 + .../internal/common/di/CoreNetworkModule.kt | 11 +- .../domain/relay/RelayJsonRpcInteractor.kt | 273 ++++++++++-------- .../android/pairing/client/PairingProtocol.kt | 17 +- .../pairing/engine/domain/PairingEngine.kt | 37 ++- .../android/relay/ConnectionState.kt | 4 +- .../android/relay/RelayClient.kt | 1 + .../internal/domain/RelayerInteractorTest.kt | 7 +- .../foundation/network/BaseRelayClient.kt | 6 +- .../notify/engine/NotifyEngine.kt | 5 +- .../domain/WatchSubscriptionsUseCase.kt | 23 +- .../com/walletconnect/sign/di/CallsModule.kt | 3 +- .../sign/engine/domain/SignEngine.kt | 55 ++-- .../use_case/calls/RejectSessionUseCase.kt | 4 + 16 files changed, 335 insertions(+), 196 deletions(-) create mode 100644 core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt new file mode 100644 index 000000000..482d506d8 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt @@ -0,0 +1,82 @@ +@file:JvmSynthetic + +package com.walletconnect.android.internal.common.connection + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import com.tinder.scarlet.Lifecycle +import com.tinder.scarlet.ShutdownReason +import com.tinder.scarlet.lifecycle.LifecycleRegistry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit + +internal class DefaultConnectionLifecycle( + application: Application, + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry() +) : Lifecycle by lifecycleRegistry { + private val job = SupervisorJob() + private var scope = CoroutineScope(job + Dispatchers.Default) + + init { + application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks()) + } + + fun connect() { + println("kobe: ApplicationResumedLifecycle; connect()") + + lifecycleRegistry.onNext(Lifecycle.State.Started) + } + + fun disconnect() { + println("kobe: ApplicationResumedLifecycle; disconnect()") + + lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason(ShutdownReason(1000, "App is paused"))) + } + + private inner class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + var isResumed: Boolean = false + var job: Job? = null + + override fun onActivityPaused(activity: Activity) { + isResumed = false + + job = scope.launch { + delay(TimeUnit.SECONDS.toMillis(30)) + if (!isResumed) { + println("kobe: onPaused; disconnect()") + lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason(ShutdownReason(1000, "App is paused"))) + job = null + } + } + } + + override fun onActivityResumed(activity: Activity) { + isResumed = true + + if (job?.isActive == true) { + job?.cancel() + job = null + } + + //todo: should auto-connect on resume when subscriptions are present +// println("kobe: onResume; connect()") +// lifecycleRegistry.onNext(Lifecycle.State.Started) + } + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityDestroyed(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + } +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/AndroidCommonDITags.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/AndroidCommonDITags.kt index 2fcb45460..fdf61faf6 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/AndroidCommonDITags.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/AndroidCommonDITags.kt @@ -10,7 +10,7 @@ enum class AndroidCommonDITags { SCARLET, MSG_ADAPTER, MANUAL_CONNECTION_LIFECYCLE, - AUTOMATIC_CONNECTION_LIFECYCLE, + DEFAULT_CONNECTION_LIFECYCLE, LOGGER, CONNECTIVITY_STATE, PUSH_RETROFIT, diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt index 15ad468fe..d58872313 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt @@ -27,6 +27,7 @@ fun coreJsonRpcModule() = module { jsonRpcHistory = get(), pushMessageStorage = get(), logger = get(named(AndroidCommonDITags.LOGGER)), + connectionLifecycle = get(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) ) } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt index ba56b0fda..1a7c7b9a1 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt @@ -6,12 +6,12 @@ import com.pandulapeter.beagle.logOkHttp.BeagleOkHttpLogger import com.squareup.moshi.Moshi import com.tinder.scarlet.Lifecycle import com.tinder.scarlet.Scarlet -import com.tinder.scarlet.lifecycle.android.AndroidLifecycle import com.tinder.scarlet.messageadapter.moshi.MoshiMessageAdapter import com.tinder.scarlet.retry.ExponentialBackoffStrategy import com.tinder.scarlet.websocket.okhttp.newWebSocketFactory import com.walletconnect.android.BuildConfig import com.walletconnect.android.internal.common.connection.ConnectivityState +import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle import com.walletconnect.android.internal.common.jwt.clientid.GenerateJwtStoreClientIdUseCase import com.walletconnect.android.relay.ConnectionType @@ -109,8 +109,9 @@ fun coreAndroidNetworkModule(serverUrl: String, connectionType: ConnectionType, ManualConnectionLifecycle() } - single(named(AndroidCommonDITags.AUTOMATIC_CONNECTION_LIFECYCLE)) { - AndroidLifecycle.ofApplicationForeground(androidApplication()) + single(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) { + DefaultConnectionLifecycle(androidApplication()) +// AndroidLifecycle.ofApplicationForeground(androidApplication()) //todo: combine with connectivity check? } single { ExponentialBackoffStrategy(INIT_BACKOFF_MILLIS, TimeUnit.SECONDS.toMillis(MAX_BACKOFF_SEC)) } @@ -136,9 +137,9 @@ fun coreAndroidNetworkModule(serverUrl: String, connectionType: ConnectionType, } } -private fun Scope.getLifecycle(connectionType: ConnectionType) = +private fun Scope.getLifecycle(connectionType: ConnectionType): Lifecycle = if (connectionType == ConnectionType.MANUAL) { get(named(AndroidCommonDITags.MANUAL_CONNECTION_LIFECYCLE)) } else { - get(named(AndroidCommonDITags.AUTOMATIC_CONNECTION_LIFECYCLE)) + get(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt index 8210cdeac..13a74b01e 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt @@ -1,12 +1,11 @@ package com.walletconnect.android.internal.common.json_rpc.domain.relay import com.walletconnect.android.internal.common.JsonRpcResponse +import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.crypto.codec.Codec import com.walletconnect.android.internal.common.crypto.sha256 -import com.walletconnect.android.internal.common.exception.NoConnectivityException import com.walletconnect.android.internal.common.exception.NoInternetConnectionException import com.walletconnect.android.internal.common.exception.NoRelayConnectionException -import com.walletconnect.android.internal.common.exception.Uncategorized import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer import com.walletconnect.android.internal.common.json_rpc.model.toRelay import com.walletconnect.android.internal.common.json_rpc.model.toWCRequest @@ -40,6 +39,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -61,6 +62,7 @@ internal class RelayJsonRpcInteractor( private val jsonRpcHistory: JsonRpcHistory, private val pushMessageStorage: PushMessagesRepository, private val logger: Logger, + private val connectionLifecycle: DefaultConnectionLifecycle ) : RelayJsonRpcInteractorInterface { private val serializer: JsonRpcSerializer get() = wcKoinApp.koin.get() @@ -80,6 +82,8 @@ internal class RelayJsonRpcInteractor( manageSubscriptions() } + //TODO: create connectIfDisconnected() method + add connectivity checks for manual and automatic modes + override fun checkConnectionWorking() { if (relay.isNetworkAvailable.value != null && relay.isNetworkAvailable.value == false) { throw NoInternetConnectionException("Connection error: Please check your Internet connection") @@ -98,6 +102,27 @@ internal class RelayJsonRpcInteractor( } } + private fun connectAndCallRelay(action: () -> Unit) { + if (relay.wssConnectionState.value is WSSConnectionState.Disconnected) { + println("kobe: connectAndCallRelay: connect()") + connectionLifecycle.connect() + + scope.launch { + supervisorScope {//todo: timeout(?) + relay.wssConnectionState + .filterIsInstance() + .first { + println("kobe: connected + action") + action() + true + } + } + } + } else if (relay.wssConnectionState.value is WSSConnectionState.Connected) { + action() + } + } + override fun publishJsonRpcRequest( topic: Topic, params: IrnParams, @@ -107,36 +132,30 @@ internal class RelayJsonRpcInteractor( onSuccess: () -> Unit, onFailure: (Throwable) -> Unit, ) { - try { - checkConnectionWorking() - } catch (e: NoConnectivityException) { - return onFailure(e) - } - - val requestJson = try { - serializer.serialize(payload) ?: return onFailure(IllegalStateException("JsonRpcInteractor: Unknown result params")) - } catch (e: Exception) { - return onFailure(e) - } - - try { - if (jsonRpcHistory.setRequest(payload.id, topic, payload.method, requestJson, TransportType.RELAY)) { - val encryptedRequest = chaChaPolyCodec.encrypt(topic, requestJson, envelopeType, participants) - val encryptedRequestString = Base64.toBase64String(encryptedRequest) - - relay.publish(topic.value, encryptedRequestString, params.toRelay()) { result -> - result.fold( - onSuccess = { onSuccess() }, - onFailure = { error -> - logger.error("JsonRpcInteractor: Cannot send the request, error: $error") - onFailure(Throwable("Publish error: ${error.message}")) - } - ) + connectAndCallRelay { + try { + val requestJson = serializer.serialize(payload) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Request Params") + + println("kobe: Request: $requestJson") + + if (jsonRpcHistory.setRequest(payload.id, topic, payload.method, requestJson, TransportType.RELAY)) { + val encryptedRequest = chaChaPolyCodec.encrypt(topic, requestJson, envelopeType, participants) + val encryptedRequestString = Base64.toBase64String(encryptedRequest) + + relay.publish(topic.value, encryptedRequestString, params.toRelay()) { result -> + result.fold( + onSuccess = { onSuccess() }, + onFailure = { error -> + logger.error("JsonRpcInteractor: Cannot send the request, error: $error") + onFailure(Throwable("Publish error: ${error.message}")) + } + ) + } } + } catch (e: Exception) { + logger.error("JsonRpcInteractor: Cannot send the request, exception: $e") + onFailure(Throwable("Publish Request Error: $e")) } - } catch (e: Exception) { - logger.error("JsonRpcInteractor: Cannot send the request, exception: $e") - return onFailure(e) } } @@ -149,32 +168,103 @@ internal class RelayJsonRpcInteractor( participants: Participants?, envelopeType: EnvelopeType, ) { - try { - checkConnectionWorking() - } catch (e: NoConnectivityException) { - return onFailure(e) + connectAndCallRelay { + try { + val responseJson = serializer.serialize(response) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Response Params") + val encryptedResponse = chaChaPolyCodec.encrypt(topic, responseJson, envelopeType, participants) + val encryptedResponseString = Base64.toBase64String(encryptedResponse) + + println("kobe: Response: $responseJson") + + relay.publish(topic.value, encryptedResponseString, params.toRelay()) { result -> + result.fold( + onSuccess = { + jsonRpcHistory.updateRequestWithResponse(response.id, responseJson) + onSuccess() + }, + onFailure = { error -> + logger.error("JsonRpcInteractor: Cannot send the response, error: $error") + onFailure(Throwable("Publish error: ${error.message}")) + } + ) + } + } catch (e: Exception) { + logger.error("JsonRpcInteractor: Cannot send the response, exception: $e") + onFailure(Throwable("Publish Response Error: $e")) + } } + } - try { - val responseJson = serializer.serialize(response) ?: return onFailure(IllegalStateException("JsonRpcInteractor: Unknown result params")) - val encryptedResponse = chaChaPolyCodec.encrypt(topic, responseJson, envelopeType, participants) - val encryptedResponseString = Base64.toBase64String(encryptedResponse) - - relay.publish(topic.value, encryptedResponseString, params.toRelay()) { result -> - result.fold( - onSuccess = { - jsonRpcHistory.updateRequestWithResponse(response.id, responseJson) - onSuccess() - }, - onFailure = { error -> - logger.error("JsonRpcInteractor: Cannot send the response, error: $error") - onFailure(Throwable("Publish error: ${error.message}")) + override fun subscribe(topic: Topic, onSuccess: (Topic) -> Unit, onFailure: (Throwable) -> Unit) { + connectAndCallRelay { + try { + relay.subscribe(topic.value) { result -> + result.fold( + onSuccess = { acknowledgement -> + subscriptions[topic.value] = acknowledgement.result + println("kobe: subscribe: success") + onSuccess(topic) + }, + onFailure = { error -> + logger.error("Subscribe to topic error: $topic error: $error") + onFailure(Throwable("Subscribe error: ${error.message}")) + } + ) + } + } catch (e: Exception) { + logger.error("Subscribe to topic error: $topic error: $e") + onFailure(Throwable("Subscribe error: ${e.message}")) + } + } + } + + override fun batchSubscribe(topics: List, onSuccess: (List) -> Unit, onFailure: (Throwable) -> Unit) { + if (topics.isNotEmpty()) { + connectAndCallRelay { + try { + relay.batchSubscribe(topics) { result -> + result.fold( + onSuccess = { acknowledgement -> + subscriptions.plusAssign(topics.zip(acknowledgement.result).toMap()) + onSuccess(topics) + }, + onFailure = { error -> + logger.error("Batch subscribe to topics error: $topics error: $error") + onFailure(Throwable("Batch subscribe error: ${error.message}")) + } + ) } - ) + } catch (e: Exception) { + logger.error("Batch subscribe to topics error: $topics error: $e") + onFailure(Throwable("Batch subscribe error: ${e.message}")) + } + } + } + } + + override fun unsubscribe(topic: Topic, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { + if (subscriptions.contains(topic.value)) { + connectAndCallRelay { + val subscriptionId = SubscriptionId(subscriptions[topic.value].toString()) + relay.unsubscribe(topic.value, subscriptionId.id) { result -> + result.fold( + onSuccess = { + scope.launch { + supervisorScope { + jsonRpcHistory.deleteRecordsByTopic(topic) + subscriptions.remove(topic.value) + pushMessageStorage.deletePushMessagesByTopic(topic.value) + onSuccess() + } + } + }, + onFailure = { error -> + logger.error("Unsubscribe to topic: $topic error: $error") + onFailure(Throwable("Unsubscribe error: ${error.message}")) + } + ) + } } - } catch (e: Exception) { - logger.error("JsonRpcInteractor: Cannot send the response, exception: $e") - return onFailure(e) } } @@ -280,82 +370,6 @@ internal class RelayJsonRpcInteractor( } } - override fun subscribe(topic: Topic, onSuccess: (Topic) -> Unit, onFailure: (Throwable) -> Unit) { - try { - checkConnectionWorking() - } catch (e: NoConnectivityException) { - return onFailure(e) - } - - relay.subscribe(topic.value) { result -> - result.fold( - onSuccess = { acknowledgement -> - subscriptions[topic.value] = acknowledgement.result - onSuccess(topic) - }, - onFailure = { error -> - logger.error("Subscribe to topic error: $topic error: $error") - onFailure(Throwable("Subscribe error: ${error.message}")) - } - ) - } - } - - override fun batchSubscribe(topics: List, onSuccess: (List) -> Unit, onFailure: (Throwable) -> Unit) { - try { - checkConnectionWorking() - } catch (e: NoConnectivityException) { - return onFailure(e) - } - - if (topics.isNotEmpty()) { - relay.batchSubscribe(topics) { result -> - result.fold( - onSuccess = { acknowledgement -> - subscriptions.plusAssign(topics.zip(acknowledgement.result).toMap()) - onSuccess(topics) - }, - onFailure = { error -> - logger.error("Batch subscribe to topics error: $topics error: $error") - onFailure(Throwable("Batch subscribe error: ${error.message}")) - } - ) - } - } - } - - override fun unsubscribe(topic: Topic, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { - try { - checkConnectionWorking() - } catch (e: NoConnectivityException) { - return onFailure(e) - } - - if (subscriptions.contains(topic.value)) { - val subscriptionId = SubscriptionId(subscriptions[topic.value].toString()) - relay.unsubscribe(topic.value, subscriptionId.id) { result -> - result.fold( - onSuccess = { - scope.launch { - supervisorScope { - jsonRpcHistory.deleteRecordsByTopic(topic) - subscriptions.remove(topic.value) - pushMessageStorage.deletePushMessagesByTopic(topic.value) - onSuccess() - } - } - }, - onFailure = { error -> - logger.error("Unsubscribe to topic: $topic error: $error") - onFailure(Throwable("Unsubscribe error: ${error.message}")) - } - ) - } - } else { - onFailure(NoSuchElementException(Uncategorized.NoMatchingTopic("Session", topic.value).message)) - } - } - private fun manageSubscriptions() { scope.launch { relay.subscriptionRequest.map { relayRequest -> @@ -365,6 +379,9 @@ internal class RelayJsonRpcInteractor( storePushRequestsIfEnabled(relayRequest, topic) Subscription(decryptMessage(topic, relayRequest), relayRequest.message, topic, relayRequest.publishedAt, relayRequest.attestation) }.collect { subscription -> + + println("kobe: Message: ${subscription.decryptedMessage}") + if (subscription.decryptedMessage.isNotEmpty()) { try { manageSubscriptions(subscription) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt index 6888983b2..cda79568c 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt @@ -77,22 +77,22 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) checkEngineInitialization() scope.launch(Dispatchers.IO) { - awaitConnection( - { +// awaitConnection( +// { try { pairingEngine.pair( uri = pair.uri, onSuccess = { onSuccess(pair) }, - onFailure = { error -> onError(Core.Model.Error(error)) } + onFailure = { error -> onError(Core.Model.Error(Throwable("Pairing error: ${error.message}"))) } ) } catch (e: Exception) { onError(Core.Model.Error(e)) } - }, - { throwable -> - logger.error(throwable) - onError(Core.Model.Error(Throwable("Pairing error: ${throwable.message}"))) - }) +// }, +// { throwable -> +// logger.error(throwable) +// onError(Core.Model.Error(Throwable("Pairing error: ${throwable.message}"))) +// }) } } @@ -159,6 +159,7 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) onConnection() return@withTimeout } + //todo: return an error after earlier - after tries ? } else { insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION)) errorLambda(Throwable("No internet connection")) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt index d6b3c3c51..ceabd2f00 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt @@ -45,7 +45,6 @@ import com.walletconnect.android.pulse.model.EventType import com.walletconnect.android.pulse.model.Trace import com.walletconnect.android.pulse.model.properties.Properties import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.android.relay.WSSConnectionState import com.walletconnect.foundation.common.model.Topic import com.walletconnect.foundation.common.model.Ttl import com.walletconnect.foundation.util.Logger @@ -63,7 +62,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -297,24 +295,37 @@ internal class PairingEngine( } private fun resubscribeToPairingTopics() { - jsonRpcInteractor.wssConnectionState - .filterIsInstance() - .onEach { - supervisorScope { - launch(Dispatchers.IO) { - sendBatchSubscribeForPairings() - } + scope.launch { + supervisorScope { + launch(Dispatchers.IO) { + sendBatchSubscribeForPairings() } + } - if (jsonRpcRequestsJob == null) { - jsonRpcRequestsJob = collectJsonRpcRequestsFlow() - } - }.launchIn(scope) + if (jsonRpcRequestsJob == null) { + jsonRpcRequestsJob = collectJsonRpcRequestsFlow() + } + } + +// jsonRpcInteractor.wssConnectionState +// .filterIsInstance() +// .onEach { +// supervisorScope { +// launch(Dispatchers.IO) { +// sendBatchSubscribeForPairings() +// } +// } +// +// if (jsonRpcRequestsJob == null) { +// jsonRpcRequestsJob = collectJsonRpcRequestsFlow() +// } +// }.launchIn(scope) } private suspend fun sendBatchSubscribeForPairings() { try { val pairingTopics = pairingRepository.getListOfPairings().filter { pairing -> pairing.isNotExpired() }.map { pairing -> pairing.topic.value } + println("kobe: re-subscribing to pairing topics: $pairingTopics") jsonRpcInteractor.batchSubscribe(pairingTopics) { error -> scope.launch { internalErrorFlow.emit(SDKError(error)) } } } catch (e: Exception) { scope.launch { internalErrorFlow.emit(SDKError(e)) } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/ConnectionState.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/ConnectionState.kt index 6583cd536..14d84e097 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/ConnectionState.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/ConnectionState.kt @@ -3,10 +3,10 @@ package com.walletconnect.android.relay sealed class WSSConnectionState { - object Connected : WSSConnectionState() + data object Connected : WSSConnectionState() sealed class Disconnected : WSSConnectionState() { data class ConnectionFailed(val throwable: Throwable) : Disconnected() data class ConnectionClosed(val message: String? = null) : Disconnected() } -} +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt index dd39a8e98..5200dc97f 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt @@ -36,6 +36,7 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC this.connectionType = connectionType logger = koinApp.koin.get(named(AndroidCommonDITags.LOGGER)) relayService = koinApp.koin.get(named(AndroidCommonDITags.RELAY_SERVICE)) + collectConnectionInitializationErrors { error -> onError(error) } monitorConnectionState() observeResults() diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt index 58eef91ea..7e917f04f 100644 --- a/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt @@ -1,6 +1,7 @@ package com.walletconnect.android.internal.domain import com.walletconnect.android.internal.common.JsonRpcResponse +import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.crypto.codec.Codec import com.walletconnect.android.internal.common.exception.WalletConnectException import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer @@ -40,6 +41,8 @@ internal class RelayerInteractorTest { every { subscriptionRequest } returns flow { } } + private val defaultConnectionLifecycle: DefaultConnectionLifecycle = mockk() + private val jsonRpcHistory: JsonRpcHistory = mockk { every { setRequest(any(), any(), any(), any(), any()) } returns true every { updateRequestWithResponse(any(), any()) } returns mockk() @@ -70,7 +73,7 @@ internal class RelayerInteractorTest { } private val sut = - spyk(RelayJsonRpcInteractor(relay, codec, jsonRpcHistory, pushMessagesRepository, logger), recordPrivateCalls = true) { + spyk(RelayJsonRpcInteractor(relay, codec, jsonRpcHistory, pushMessagesRepository, logger, defaultConnectionLifecycle), recordPrivateCalls = true) { every { checkConnectionWorking() } answers { } } @@ -86,7 +89,7 @@ internal class RelayerInteractorTest { every { topic } returns topicVO } - val peerError: Error = mockk { + private val peerError: Error = mockk { every { message } returns "message" every { code } returns -1 } diff --git a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt index f8289b3af..2d4c487f5 100644 --- a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt +++ b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt @@ -74,9 +74,9 @@ abstract class BaseRelayClient : RelayInterface { relayService .observeWebSocketEvent() .map { event -> - if (isLoggingEnabled) { - println("Event: $event") - } +// if (isLoggingEnabled) { + println("kobe: Event: $event") +// } event.toRelayEvent() } .shareIn(scope, SharingStarted.Lazily, REPLAY) diff --git a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt index ec22f9324..c31e85df4 100644 --- a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt +++ b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt @@ -107,8 +107,9 @@ internal class NotifyEngine( .onEach { supervisorScope { launch(Dispatchers.IO) { - resubscribeToSubscriptions() - watchSubscriptionsForEveryRegisteredAccount() +// println("kobe: Notify batch subs") +// resubscribeToSubscriptions() +// watchSubscriptionsForEveryRegisteredAccount() } } diff --git a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt index da68fcd12..60f982e4a 100644 --- a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt +++ b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt @@ -4,9 +4,7 @@ package com.walletconnect.notify.engine.domain import com.walletconnect.android.internal.common.crypto.kmr.KeyManagementRepository import com.walletconnect.android.internal.common.model.AccountId -import com.walletconnect.android.internal.common.model.EnvelopeType import com.walletconnect.android.internal.common.model.IrnParams -import com.walletconnect.android.internal.common.model.Participants import com.walletconnect.android.internal.common.model.Tags import com.walletconnect.android.internal.common.model.params.CoreNotifyParams import com.walletconnect.android.internal.common.model.type.RelayJsonRpcInteractorInterface @@ -35,7 +33,8 @@ internal class WatchSubscriptionsUseCase( val selfPublicKey = getSelfKeyForWatchSubscriptionUseCase(requestTopic, accountId) val responseTopic = keyManagementRepository.generateTopicFromKeyAgreement(selfPublicKey, peerPublicKey) - jsonRpcInteractor.subscribe(responseTopic) { error -> onFailure(error) } +// println("kobe: watch") +// jsonRpcInteractor.subscribe(responseTopic) { error -> onFailure(error) } val account = registeredAccountsRepository.getAccountByAccountId(accountId.value) val didJwt = fetchDidJwtInteractor.watchSubscriptionsRequest(accountId, authenticationPublicKey, account.appDomain) @@ -46,14 +45,14 @@ internal class WatchSubscriptionsUseCase( val request = NotifyRpc.NotifyWatchSubscriptions(params = watchSubscriptionsParams) val irnParams = IrnParams(Tags.NOTIFY_WATCH_SUBSCRIPTIONS, Ttl(thirtySeconds)) - jsonRpcInteractor.publishJsonRpcRequest( - topic = requestTopic, - params = irnParams, - payload = request, - envelopeType = EnvelopeType.ONE, - participants = Participants(selfPublicKey, peerPublicKey), - onSuccess = onSuccess, - onFailure = onFailure - ) +// jsonRpcInteractor.publishJsonRpcRequest( +// topic = requestTopic, +// params = irnParams, +// payload = request, +// envelopeType = EnvelopeType.ONE, +// participants = Participants(selfPublicKey, peerPublicKey), +// onSuccess = onSuccess, +// onFailure = onFailure +// ) } } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/CallsModule.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/CallsModule.kt index 454e431f0..d6237dfec 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/CallsModule.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/CallsModule.kt @@ -136,7 +136,8 @@ internal fun callsModule() = module { verifyContextStorageRepository = get(), proposalStorageRepository = get(), jsonRpcInteractor = get(), - logger = get(named(AndroidCommonDITags.LOGGER)) + logger = get(named(AndroidCommonDITags.LOGGER)), + pairingController = get() ) } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt index f117e1b05..82ea66e9c 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt @@ -83,7 +83,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge @@ -216,24 +215,42 @@ internal class SignEngine( } private fun handleRelayRequestsAndResponses() { - jsonRpcInteractor.wssConnectionState - .filterIsInstance() - .onEach { - supervisorScope { - launch(Dispatchers.IO) { - resubscribeToSession() - resubscribeToPendingAuthenticateTopics() - } + scope.launch { + supervisorScope { + launch(Dispatchers.IO) { + resubscribeToSession() + resubscribeToPendingAuthenticateTopics() } + } - if (jsonRpcRequestsJob == null) { - jsonRpcRequestsJob = collectJsonRpcRequests() - } + if (jsonRpcRequestsJob == null) { + jsonRpcRequestsJob = collectJsonRpcRequests() + } - if (jsonRpcResponsesJob == null) { - jsonRpcResponsesJob = collectJsonRpcResponses() - } - }.launchIn(scope) + if (jsonRpcResponsesJob == null) { + jsonRpcResponsesJob = collectJsonRpcResponses() + } + } + + +// jsonRpcInteractor.wssConnectionState +// .filterIsInstance() +// .onEach { +// supervisorScope { +// launch(Dispatchers.IO) { +// resubscribeToSession() +// resubscribeToPendingAuthenticateTopics() +// } +// } +// +// if (jsonRpcRequestsJob == null) { +// jsonRpcRequestsJob = collectJsonRpcRequests() +// } +// +// if (jsonRpcResponsesJob == null) { +// jsonRpcResponsesJob = collectJsonRpcResponses() +// } +// }.launchIn(scope) } private fun handleLinkModeResponses() { @@ -325,13 +342,12 @@ internal class SignEngine( listOfExpiredSession .map { session -> session.topic } .onEach { sessionTopic -> - runCatching { - crypto.removeKeys(sessionTopic.value) - }.onFailure { logger.error(it) } + runCatching { crypto.removeKeys(sessionTopic.value) }.onFailure { logger.error(it) } sessionStorageRepository.deleteSession(sessionTopic) } val validSessionTopics = listOfValidSessions.map { it.topic.value } + println("kobe: re-subscribe to session topics: $validSessionTopics") jsonRpcInteractor.batchSubscribe(validSessionTopics) { error -> scope.launch { _engineEvent.emit(SDKError(error)) } } } catch (e: Exception) { scope.launch { _engineEvent.emit(SDKError(e)) } @@ -342,6 +358,7 @@ internal class SignEngine( scope.launch { try { val responseTopics = authenticateResponseTopicRepository.getResponseTopics().map { responseTopic -> responseTopic } + println("kobe: re-subscribe to pending auth topics; $responseTopics") jsonRpcInteractor.batchSubscribe(responseTopics) { error -> scope.launch { _engineEvent.emit(SDKError(error)) } } } catch (e: Exception) { scope.launch { _engineEvent.emit(SDKError(e)) } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionUseCase.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionUseCase.kt index 8517eb6dd..16466d5e3 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionUseCase.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionUseCase.kt @@ -1,5 +1,6 @@ package com.walletconnect.sign.engine.use_case.calls +import com.walletconnect.android.Core import com.walletconnect.android.internal.common.model.IrnParams import com.walletconnect.android.internal.common.model.Tags import com.walletconnect.android.internal.common.model.type.RelayJsonRpcInteractorInterface @@ -7,6 +8,7 @@ import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.storage.verify.VerifyContextStorageRepository import com.walletconnect.android.internal.utils.CoreValidator.isExpired import com.walletconnect.android.internal.utils.fiveMinutesInSeconds +import com.walletconnect.android.pairing.handler.PairingControllerInterface import com.walletconnect.foundation.common.model.Ttl import com.walletconnect.foundation.util.Logger import com.walletconnect.sign.common.exceptions.PeerError @@ -20,6 +22,7 @@ internal class RejectSessionUseCase( private val verifyContextStorageRepository: VerifyContextStorageRepository, private val jsonRpcInteractor: RelayJsonRpcInteractorInterface, private val proposalStorageRepository: ProposalStorageRepository, + private val pairingController: PairingControllerInterface, private val logger: Logger ) : RejectSessionUseCaseInterface { @@ -42,6 +45,7 @@ internal class RejectSessionUseCase( scope.launch { proposalStorageRepository.deleteProposal(proposerPublicKey) verifyContextStorageRepository.delete(proposal.requestId) + pairingController.deleteAndUnsubscribePairing(Core.Params.Delete(proposal.pairingTopic.value)) } onSuccess() }, From 6b147a8f720c348d4f174d1bd8061ca5b03904f0 Mon Sep 17 00:00:00 2001 From: kubel Date: Thu, 29 Aug 2024 14:13:15 +0200 Subject: [PATCH 2/7] Open connection lifecycle --- .../com/walletconnect/android/CoreProtocol.kt | 2 +- .../ConditionalExponentialBackoffStrategy.kt | 24 ++++ .../common/connection/ConnectionLifecycle.kt | 8 ++ .../connection/DefaultConnectionLifecycle.kt | 30 +++-- .../connection/ManualConnectionLifecycle.kt | 9 +- .../internal/common/di/CoreJsonRpcModule.kt | 18 ++- .../internal/common/di/CoreNetworkModule.kt | 7 +- .../domain/relay/RelayJsonRpcInteractor.kt | 124 ++++++++++++------ .../type/RelayJsonRpcInteractorInterface.kt | 4 +- .../android/internal/utils/ObservableMap.kt | 19 +++ .../android/pairing/client/PairingProtocol.kt | 55 ++++---- .../pairing/engine/domain/PairingEngine.kt | 33 ++--- .../android/relay/RelayClient.kt | 15 ++- .../walletconnect/android/utils/Extensions.kt | 6 +- .../internal/domain/RelayerInteractorTest.kt | 7 +- gradle/libs.versions.toml | 16 +-- .../notify/engine/NotifyEngine.kt | 5 +- .../walletconnect/sign/client/SignProtocol.kt | 5 + .../sign/engine/domain/SignEngine.kt | 51 +++---- .../sample/wallet/Web3WalletApplication.kt | 3 +- .../sample/wallet/domain/WCDelegate.kt | 1 - .../sample/wallet/ui/Web3WalletViewModel.kt | 14 +- .../wallet/ui/routes/host/WalletSampleHost.kt | 20 +-- 23 files changed, 291 insertions(+), 185 deletions(-) create mode 100644 core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt create mode 100644 core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt create mode 100644 core/android/src/main/kotlin/com/walletconnect/android/internal/utils/ObservableMap.kt diff --git a/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt b/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt index d90923d32..cf5fddf4f 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt @@ -176,7 +176,7 @@ class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInter module { single { Echo } }, module { single { Push } }, module { single { Verify } }, - coreJsonRpcModule(), + coreJsonRpcModule(connectionType), corePairingModule(Pairing, PairingController), keyServerModule(keyServerUrl), explorerModule(), diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt new file mode 100644 index 000000000..72a7978d9 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt @@ -0,0 +1,24 @@ +package com.walletconnect.android.internal.common + +import com.tinder.scarlet.retry.BackoffStrategy +import kotlin.math.pow + +class ConditionalExponentialBackoffStrategy( + private val initialDurationMillis: Long, + private val maxDurationMillis: Long +) : BackoffStrategy { + init { + require(initialDurationMillis > 0) { "initialDurationMillis, $initialDurationMillis, must be positive" } + require(maxDurationMillis > 0) { "maxDurationMillis, $maxDurationMillis, must be positive" } + } + + override var shouldBackoff: Boolean = false + + fun shouldBackoff(shouldBackoff: Boolean) { + println("kobe:be Sending shouldBackoff: $shouldBackoff") + this.shouldBackoff = shouldBackoff + } + + override fun backoffDurationMillisAt(retryCount: Int): Long = + maxDurationMillis.toDouble().coerceAtMost(initialDurationMillis.toDouble() * 2.0.pow(retryCount.toDouble())).toLong() +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt new file mode 100644 index 000000000..dcd3fc8d9 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt @@ -0,0 +1,8 @@ +package com.walletconnect.android.internal.common.connection + +import kotlinx.coroutines.flow.StateFlow + +interface ConnectionLifecycle { + val onResume: StateFlow + fun reconnect() +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt index 482d506d8..48b80f918 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt @@ -13,37 +13,38 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit internal class DefaultConnectionLifecycle( application: Application, private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry() -) : Lifecycle by lifecycleRegistry { +) : Lifecycle by lifecycleRegistry, ConnectionLifecycle { private val job = SupervisorJob() private var scope = CoroutineScope(job + Dispatchers.Default) + private val _onResume = MutableStateFlow(null) + override val onResume: StateFlow = _onResume.asStateFlow() + init { application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks()) } - fun connect() { - println("kobe: ApplicationResumedLifecycle; connect()") - + override fun reconnect() { + println("kobe: reconnect()") + lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason()) lifecycleRegistry.onNext(Lifecycle.State.Started) } - fun disconnect() { - println("kobe: ApplicationResumedLifecycle; disconnect()") - - lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason(ShutdownReason(1000, "App is paused"))) - } - private inner class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { var isResumed: Boolean = false var job: Job? = null override fun onActivityPaused(activity: Activity) { + println("kobe: pause") isResumed = false job = scope.launch { @@ -52,11 +53,13 @@ internal class DefaultConnectionLifecycle( println("kobe: onPaused; disconnect()") lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason(ShutdownReason(1000, "App is paused"))) job = null + _onResume.value = false } } } override fun onActivityResumed(activity: Activity) { + println("kobe: resume") isResumed = true if (job?.isActive == true) { @@ -64,9 +67,10 @@ internal class DefaultConnectionLifecycle( job = null } - //todo: should auto-connect on resume when subscriptions are present -// println("kobe: onResume; connect()") -// lifecycleRegistry.onNext(Lifecycle.State.Started) + + scope.launch { + _onResume.value = true + } } override fun onActivityStarted(activity: Activity) {} diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt index 395a04df7..2d3076761 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt @@ -4,10 +4,12 @@ package com.walletconnect.android.internal.common.connection import com.tinder.scarlet.Lifecycle import com.tinder.scarlet.lifecycle.LifecycleRegistry +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow internal class ManualConnectionLifecycle( private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(), -) : Lifecycle by lifecycleRegistry { +) : Lifecycle by lifecycleRegistry, ConnectionLifecycle { fun connect() { lifecycleRegistry.onNext(Lifecycle.State.Started) } @@ -16,7 +18,10 @@ internal class ManualConnectionLifecycle( lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason()) } - fun restart() { + override val onResume: StateFlow + get() = MutableStateFlow(null) + + override fun reconnect() { lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason()) lifecycleRegistry.onNext(Lifecycle.State.Started) } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt index d58872313..c798938ba 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt @@ -1,6 +1,9 @@ package com.walletconnect.android.internal.common.di import com.squareup.moshi.Moshi +import com.walletconnect.android.internal.common.connection.ConnectionLifecycle +import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle +import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer import com.walletconnect.android.internal.common.json_rpc.domain.link_mode.LinkModeJsonRpcInteractor import com.walletconnect.android.internal.common.json_rpc.domain.link_mode.LinkModeJsonRpcInteractorInterface @@ -9,16 +12,26 @@ import com.walletconnect.android.internal.common.model.type.RelayJsonRpcInteract import com.walletconnect.android.internal.common.model.type.SerializableJsonRpc import com.walletconnect.android.pairing.model.PairingJsonRpcMethod import com.walletconnect.android.pairing.model.PairingRpc +import com.walletconnect.android.relay.ConnectionType import com.walletconnect.utils.JsonAdapterEntry import com.walletconnect.utils.addDeserializerEntry import com.walletconnect.utils.addSerializerEntry import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named +import org.koin.core.scope.Scope import org.koin.dsl.module import kotlin.reflect.KClass + +fun Scope.getConnectionLifecycle(connectionType: ConnectionType): ConnectionLifecycle = + if (connectionType == ConnectionType.MANUAL) { + get(named(AndroidCommonDITags.MANUAL_CONNECTION_LIFECYCLE)) + } else { + get(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) + } + @JvmSynthetic -fun coreJsonRpcModule() = module { +fun coreJsonRpcModule(connectionType: ConnectionType) = module { single { RelayJsonRpcInteractor( @@ -27,7 +40,8 @@ fun coreJsonRpcModule() = module { jsonRpcHistory = get(), pushMessageStorage = get(), logger = get(named(AndroidCommonDITags.LOGGER)), - connectionLifecycle = get(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) + connectionLifecycle = getConnectionLifecycle(connectionType), + backoffStrategy = get() ) } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt index 1a7c7b9a1..b72ff5fc5 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreNetworkModule.kt @@ -7,9 +7,9 @@ import com.squareup.moshi.Moshi import com.tinder.scarlet.Lifecycle import com.tinder.scarlet.Scarlet import com.tinder.scarlet.messageadapter.moshi.MoshiMessageAdapter -import com.tinder.scarlet.retry.ExponentialBackoffStrategy import com.tinder.scarlet.websocket.okhttp.newWebSocketFactory import com.walletconnect.android.BuildConfig +import com.walletconnect.android.internal.common.ConditionalExponentialBackoffStrategy import com.walletconnect.android.internal.common.connection.ConnectivityState import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle @@ -111,16 +111,15 @@ fun coreAndroidNetworkModule(serverUrl: String, connectionType: ConnectionType, single(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) { DefaultConnectionLifecycle(androidApplication()) -// AndroidLifecycle.ofApplicationForeground(androidApplication()) //todo: combine with connectivity check? } - single { ExponentialBackoffStrategy(INIT_BACKOFF_MILLIS, TimeUnit.SECONDS.toMillis(MAX_BACKOFF_SEC)) } + single { ConditionalExponentialBackoffStrategy(INIT_BACKOFF_MILLIS, TimeUnit.SECONDS.toMillis(MAX_BACKOFF_SEC)) } single { FlowStreamAdapter.Factory() } single(named(AndroidCommonDITags.SCARLET)) { Scarlet.Builder() - .backoffStrategy(get()) + .backoffStrategy((get())) .webSocketFactory(get(named(AndroidCommonDITags.OK_HTTP)).newWebSocketFactory(get(named(AndroidCommonDITags.RELAY_URL)))) .lifecycle(getLifecycle(connectionType)) .addMessageAdapterFactory(get(named(AndroidCommonDITags.MSG_ADAPTER))) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt index 13a74b01e..baa03ec72 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt @@ -1,9 +1,11 @@ package com.walletconnect.android.internal.common.json_rpc.domain.relay +import com.walletconnect.android.internal.common.ConditionalExponentialBackoffStrategy import com.walletconnect.android.internal.common.JsonRpcResponse -import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle +import com.walletconnect.android.internal.common.connection.ConnectionLifecycle import com.walletconnect.android.internal.common.crypto.codec.Codec import com.walletconnect.android.internal.common.crypto.sha256 +import com.walletconnect.android.internal.common.exception.NoConnectivityException import com.walletconnect.android.internal.common.exception.NoInternetConnectionException import com.walletconnect.android.internal.common.exception.NoRelayConnectionException import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer @@ -27,6 +29,7 @@ import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.storage.push_messages.PushMessagesRepository import com.walletconnect.android.internal.common.storage.rpc.JsonRpcHistory import com.walletconnect.android.internal.common.wcKoinApp +import com.walletconnect.android.internal.utils.ObservableMap import com.walletconnect.android.relay.RelayConnectionInterface import com.walletconnect.android.relay.WSSConnectionState import com.walletconnect.foundation.common.model.SubscriptionId @@ -34,6 +37,7 @@ import com.walletconnect.foundation.common.model.Topic import com.walletconnect.foundation.network.model.Relay import com.walletconnect.foundation.util.Logger import com.walletconnect.utils.Empty +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -43,6 +47,7 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope @@ -62,7 +67,8 @@ internal class RelayJsonRpcInteractor( private val jsonRpcHistory: JsonRpcHistory, private val pushMessageStorage: PushMessagesRepository, private val logger: Logger, - private val connectionLifecycle: DefaultConnectionLifecycle + private val connectionLifecycle: ConnectionLifecycle, + private val backoffStrategy: ConditionalExponentialBackoffStrategy ) : RelayJsonRpcInteractorInterface { private val serializer: JsonRpcSerializer get() = wcKoinApp.koin.get() @@ -76,50 +82,61 @@ internal class RelayJsonRpcInteractor( override val internalErrors: SharedFlow = _internalErrors.asSharedFlow() override val wssConnectionState: StateFlow get() = relay.wssConnectionState - private val subscriptions: MutableMap = mutableMapOf() + private var subscriptions = ObservableMap(mutableMapOf()) { newMap -> backoffStrategy.shouldBackoff(newMap.isNotEmpty()) } + override val onResubscribe: Flow + get() = merge( + connectionLifecycle.onResume.filter { isResumed -> isResumed != null && isResumed }, + relay.wssConnectionState.filterIsInstance(WSSConnectionState.Connected::class) + ) init { manageSubscriptions() } - //TODO: create connectIfDisconnected() method + add connectivity checks for manual and automatic modes - - override fun checkConnectionWorking() { + override fun checkNetworkConnectivity() { if (relay.isNetworkAvailable.value != null && relay.isNetworkAvailable.value == false) { throw NoInternetConnectionException("Connection error: Please check your Internet connection") } - - if (relay.wssConnectionState.value is WSSConnectionState.Disconnected) { - val message = when (relay.wssConnectionState.value) { - is WSSConnectionState.Disconnected.ConnectionClosed -> - (relay.wssConnectionState.value as WSSConnectionState.Disconnected.ConnectionClosed).message ?: "WSS connection closed" - - is WSSConnectionState.Disconnected.ConnectionFailed -> (relay.wssConnectionState.value as WSSConnectionState.Disconnected.ConnectionFailed).throwable.message - - else -> "WSS connection closed" - } - throw NoRelayConnectionException("Connection error: No Relay connection: $message") - } } - private fun connectAndCallRelay(action: () -> Unit) { + private fun connectAndCallRelay(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { if (relay.wssConnectionState.value is WSSConnectionState.Disconnected) { - println("kobe: connectAndCallRelay: connect()") - connectionLifecycle.connect() - + connectionLifecycle.reconnect() + //todo: timeout(?) + check how many times we try to re-connect scope.launch { - supervisorScope {//todo: timeout(?) + supervisorScope { relay.wssConnectionState - .filterIsInstance() - .first { - println("kobe: connected + action") - action() + .filter { state -> + when (state) { + is WSSConnectionState.Disconnected.ConnectionClosed -> state.message != null + else -> true + } + } + .first { state -> + println("kobe: interactor state: $state") + when (state) { + is WSSConnectionState.Connected -> { + println("kobe: action") + onConnected() + } + + is WSSConnectionState.Disconnected.ConnectionFailed -> { + println("kobe: error failed: ${state.throwable}") + onFailure(NoRelayConnectionException(state.throwable.message)) + } + + is WSSConnectionState.Disconnected.ConnectionClosed -> { + println("kobe: error closed: $state") + onFailure(NoRelayConnectionException(state.message)) + } + } true } } } } else if (relay.wssConnectionState.value is WSSConnectionState.Connected) { - action() + println("kobe: already connected") + onConnected() } } @@ -132,7 +149,13 @@ internal class RelayJsonRpcInteractor( onSuccess: () -> Unit, onFailure: (Throwable) -> Unit, ) { - connectAndCallRelay { + try { + checkNetworkConnectivity() + } catch (e: NoConnectivityException) { + return onFailure(e) + } + + connectAndCallRelay(onConnected = { try { val requestJson = serializer.serialize(payload) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Request Params") @@ -156,7 +179,7 @@ internal class RelayJsonRpcInteractor( logger.error("JsonRpcInteractor: Cannot send the request, exception: $e") onFailure(Throwable("Publish Request Error: $e")) } - } + }, onFailure = { error -> onFailure(error) }) } override fun publishJsonRpcResponse( @@ -168,7 +191,13 @@ internal class RelayJsonRpcInteractor( participants: Participants?, envelopeType: EnvelopeType, ) { - connectAndCallRelay { + try { + checkNetworkConnectivity() + } catch (e: NoConnectivityException) { + return onFailure(e) + } + + connectAndCallRelay(onConnected = { try { val responseJson = serializer.serialize(response) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Response Params") val encryptedResponse = chaChaPolyCodec.encrypt(topic, responseJson, envelopeType, participants) @@ -192,17 +221,22 @@ internal class RelayJsonRpcInteractor( logger.error("JsonRpcInteractor: Cannot send the response, exception: $e") onFailure(Throwable("Publish Response Error: $e")) } - } + }, onFailure = { error -> onFailure(error) }) } override fun subscribe(topic: Topic, onSuccess: (Topic) -> Unit, onFailure: (Throwable) -> Unit) { - connectAndCallRelay { + try { + checkNetworkConnectivity() + } catch (e: NoConnectivityException) { + return onFailure(e) + } + + connectAndCallRelay(onConnected = { try { relay.subscribe(topic.value) { result -> result.fold( onSuccess = { acknowledgement -> subscriptions[topic.value] = acknowledgement.result - println("kobe: subscribe: success") onSuccess(topic) }, onFailure = { error -> @@ -215,12 +249,18 @@ internal class RelayJsonRpcInteractor( logger.error("Subscribe to topic error: $topic error: $e") onFailure(Throwable("Subscribe error: ${e.message}")) } - } + }, onFailure = { error -> onFailure(error) }) } override fun batchSubscribe(topics: List, onSuccess: (List) -> Unit, onFailure: (Throwable) -> Unit) { + try { + checkNetworkConnectivity() + } catch (e: NoConnectivityException) { + return onFailure(e) + } + if (topics.isNotEmpty()) { - connectAndCallRelay { + connectAndCallRelay(onConnected = { try { relay.batchSubscribe(topics) { result -> result.fold( @@ -238,13 +278,19 @@ internal class RelayJsonRpcInteractor( logger.error("Batch subscribe to topics error: $topics error: $e") onFailure(Throwable("Batch subscribe error: ${e.message}")) } - } + }, onFailure = { error -> onFailure(error) }) } } override fun unsubscribe(topic: Topic, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { + try { + checkNetworkConnectivity() + } catch (e: NoConnectivityException) { + return onFailure(e) + } + if (subscriptions.contains(topic.value)) { - connectAndCallRelay { + connectAndCallRelay(onConnected = { val subscriptionId = SubscriptionId(subscriptions[topic.value].toString()) relay.unsubscribe(topic.value, subscriptionId.id) { result -> result.fold( @@ -264,7 +310,7 @@ internal class RelayJsonRpcInteractor( } ) } - } + }, onFailure = { error -> onFailure(error) }) } } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/type/RelayJsonRpcInteractorInterface.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/type/RelayJsonRpcInteractorInterface.kt index 017090b2c..7f535b97c 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/type/RelayJsonRpcInteractorInterface.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/type/RelayJsonRpcInteractorInterface.kt @@ -7,11 +7,13 @@ import com.walletconnect.android.internal.common.model.Participants import com.walletconnect.android.internal.common.model.WCRequest import com.walletconnect.android.relay.WSSConnectionState import com.walletconnect.foundation.common.model.Topic +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface RelayJsonRpcInteractorInterface : JsonRpcInteractorInterface { val wssConnectionState: StateFlow - fun checkConnectionWorking() + val onResubscribe: Flow + fun checkNetworkConnectivity() fun subscribe(topic: Topic, onSuccess: (Topic) -> Unit = {}, onFailure: (Throwable) -> Unit = {}) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/utils/ObservableMap.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/utils/ObservableMap.kt new file mode 100644 index 000000000..c492a723c --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/utils/ObservableMap.kt @@ -0,0 +1,19 @@ +package com.walletconnect.android.internal.utils + +class ObservableMap( + private val map: MutableMap = mutableMapOf(), + private val onChange: (Map) -> Unit +) : MutableMap by map { + + override fun put(key: K, value: V): V? { + return map.put(key, value).also { onChange(map) } + } + + override fun remove(key: K): V? { + return map.remove(key).also { onChange(map) } + } + + override fun putAll(from: Map) { + return map.putAll(from).also { onChange(map) } + } +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt index cda79568c..401b009e7 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt @@ -10,16 +10,11 @@ import com.walletconnect.android.pairing.engine.domain.PairingEngine import com.walletconnect.android.pairing.engine.model.EngineDO import com.walletconnect.android.pairing.model.mapper.toCore import com.walletconnect.android.pulse.domain.InsertTelemetryEventUseCase -import com.walletconnect.android.pulse.model.EventType -import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.android.relay.RelayConnectionInterface -import com.walletconnect.android.relay.WSSConnectionState import com.walletconnect.foundation.util.Logger import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout import org.koin.core.KoinApplication internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) : PairingInterface { @@ -149,31 +144,31 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) } } - private suspend fun awaitConnection(onConnection: () -> Unit, errorLambda: (Throwable) -> Unit = {}) { - try { - withTimeout(60000) { - while (true) { - if (relayClient.isNetworkAvailable.value != null) { - if (relayClient.isNetworkAvailable.value == true) { - if (relayClient.wssConnectionState.value is WSSConnectionState.Connected) { - onConnection() - return@withTimeout - } - //todo: return an error after earlier - after tries ? - } else { - insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION)) - errorLambda(Throwable("No internet connection")) - return@withTimeout - } - } - delay(100) - } - } - } catch (e: Exception) { - insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION)) - errorLambda(Throwable("Failed to connect: ${e.message}")) - } - } +// private suspend fun awaitConnection(onConnection: () -> Unit, errorLambda: (Throwable) -> Unit = {}) { +// try { +// withTimeout(60000) { +// while (true) { +// if (relayClient.isNetworkAvailable.value != null) { +// if (relayClient.isNetworkAvailable.value == true) { +// if (relayClient.wssConnectionState.value is WSSConnectionState.Connected) { +// onConnection() +// return@withTimeout +// } +// //todo: return an error after earlier - after tries ? +// } else { +// insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION)) +// errorLambda(Throwable("No internet connection")) +// return@withTimeout +// } +// } +// delay(100) +// } +// } +// } catch (e: Exception) { +// insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION)) +// errorLambda(Throwable("Failed to connect: ${e.message}")) +// } +// } @Throws(IllegalStateException::class) private fun checkEngineInitialization() { diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt index ceabd2f00..b987d4fcb 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt @@ -295,31 +295,18 @@ internal class PairingEngine( } private fun resubscribeToPairingTopics() { - scope.launch { - supervisorScope { - launch(Dispatchers.IO) { - sendBatchSubscribeForPairings() + jsonRpcInteractor.onResubscribe + .onEach { + supervisorScope { + launch(Dispatchers.IO) { + sendBatchSubscribeForPairings() + } } - } - if (jsonRpcRequestsJob == null) { - jsonRpcRequestsJob = collectJsonRpcRequestsFlow() - } - } - -// jsonRpcInteractor.wssConnectionState -// .filterIsInstance() -// .onEach { -// supervisorScope { -// launch(Dispatchers.IO) { -// sendBatchSubscribeForPairings() -// } -// } -// -// if (jsonRpcRequestsJob == null) { -// jsonRpcRequestsJob = collectJsonRpcRequestsFlow() -// } -// }.launchIn(scope) + if (jsonRpcRequestsJob == null) { + jsonRpcRequestsJob = collectJsonRpcRequestsFlow() + } + }.launchIn(scope) } private suspend fun sendBatchSubscribeForPairings() { diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt index 5200dc97f..1e7771867 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt @@ -14,10 +14,9 @@ import com.walletconnect.foundation.network.BaseRelayClient import com.walletconnect.foundation.network.model.Relay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import org.koin.core.KoinApplication @@ -27,6 +26,7 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC private val manualConnection: ManualConnectionLifecycle by lazy { koinApp.koin.get(named(AndroidCommonDITags.MANUAL_CONNECTION_LIFECYCLE)) } private val networkState: ConnectivityState by lazy { koinApp.koin.get(named(AndroidCommonDITags.CONNECTIVITY_STATE)) } override val isNetworkAvailable: StateFlow by lazy { networkState.isAvailable } + private val _wssConnectionState: MutableStateFlow = MutableStateFlow(WSSConnectionState.Disconnected.ConnectionClosed()) override val wssConnectionState: StateFlow = _wssConnectionState private lateinit var connectionType: ConnectionType @@ -46,13 +46,13 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC scope.launch { supervisorScope { eventsFlow - .takeWhile { event -> + .first { event -> if (event is Relay.Model.Event.OnConnectionFailed) { onError(event.throwable.toWalletConnectException) } event !is Relay.Model.Event.OnConnectionOpened<*> - }.collect() + } } } } @@ -69,7 +69,10 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC _wssConnectionState.value = WSSConnectionState.Connected event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Connected -> - _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable) + _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) + + event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Disconnected.ConnectionClosed -> + _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) event is Relay.Model.Event.OnConnectionClosed && _wssConnectionState.value is WSSConnectionState.Connected -> _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionClosed("Connection closed: ${event.shutdownReason.reason} ${event.shutdownReason.code}") @@ -94,7 +97,7 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC try { when (connectionType) { ConnectionType.AUTOMATIC -> onError(Core.Model.Error(IllegalStateException(WRONG_CONNECTION_TYPE))) - ConnectionType.MANUAL -> manualConnection.restart() + ConnectionType.MANUAL -> manualConnection.reconnect() } } catch (e: Exception) { onError(Core.Model.Error(e)) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/utils/Extensions.kt b/core/android/src/main/kotlin/com/walletconnect/android/utils/Extensions.kt index 2dc2c5f9a..d87b71e43 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/utils/Extensions.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/utils/Extensions.kt @@ -41,12 +41,12 @@ internal val Throwable.toWalletConnectException: WalletConnectException UnableToConnectToWebsocketException("${this.message}. It's possible that JWT has expired. Try initializing the CoreClient again.") this.message?.contains(HttpURLConnection.HTTP_NOT_FOUND.toString()) == true -> - ProjectIdDoesNotExistException(this.message) + ProjectIdDoesNotExistException("Project ID doesn't exist: ${this.message}") this.message?.contains(HttpURLConnection.HTTP_FORBIDDEN.toString()) == true -> - InvalidProjectIdException(this.message) + InvalidProjectIdException("Invalid project ID: ${this.message}") - else -> GenericException("Error while connecting, please check your Internet connection or contact support: $this") + else -> GenericException("Error while connecting, please check your Internet connection or contact support: ${this.message}") } @get:JvmSynthetic diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt index 7e917f04f..ee273e612 100644 --- a/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt @@ -1,5 +1,6 @@ package com.walletconnect.android.internal.domain +import com.walletconnect.android.internal.common.ConditionalExponentialBackoffStrategy import com.walletconnect.android.internal.common.JsonRpcResponse import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.crypto.codec.Codec @@ -43,6 +44,8 @@ internal class RelayerInteractorTest { private val defaultConnectionLifecycle: DefaultConnectionLifecycle = mockk() + private val backoffStrategy: ConditionalExponentialBackoffStrategy = mockk() + private val jsonRpcHistory: JsonRpcHistory = mockk { every { setRequest(any(), any(), any(), any(), any()) } returns true every { updateRequestWithResponse(any(), any()) } returns mockk() @@ -73,8 +76,8 @@ internal class RelayerInteractorTest { } private val sut = - spyk(RelayJsonRpcInteractor(relay, codec, jsonRpcHistory, pushMessagesRepository, logger, defaultConnectionLifecycle), recordPrivateCalls = true) { - every { checkConnectionWorking() } answers { } + spyk(RelayJsonRpcInteractor(relay, codec, jsonRpcHistory, pushMessagesRepository, logger, defaultConnectionLifecycle, backoffStrategy), recordPrivateCalls = true) { + every { checkNetworkConnectivity() } answers { } } private val topicVO = Topic("mockkTopic") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b92764b1..96fccff90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ sqlDelight = "2.0.2" dokka = "1.9.20" moshi = "1.15.1" googleService = "4.4.1" -scarlet = "1.0.1" +scarlet = "0.1.14-SNAPSHOT" #"1.0.1" koin = "3.5.6" retrofit = "2.11.0" okhttp = "4.12.0" @@ -98,13 +98,13 @@ androidx-compose-material = { module = "androidx.compose.material:material", ver coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -scarlet = { module = "com.walletconnect.Scarlet:scarlet", version.ref = "scarlet" } -scarlet-okhttp = { module = "com.walletconnect.Scarlet:websocket-okhttp", version.ref = "scarlet" } -scarlet-coroutines = { module = "com.walletconnect.Scarlet:stream-adapter-coroutines", version.ref = "scarlet" } -scarlet-moshi = { module = "com.walletconnect.Scarlet:message-adapter-moshi", version.ref = "scarlet" } -scarlet-android = { module = "com.walletconnect.Scarlet:lifecycle-android", version.ref = "scarlet" } -scarlet-mockwebserver = { module = "com.walletconnect.Scarlet:websocket-mockwebserver", version.ref = "scarlet" } -scarlet-testUtils = { module = "com.walletconnect.Scarlet:test-utils", version.ref = "scarlet" } +scarlet = { module = "com.tinder.scarlet:scarlet", version.ref = "scarlet" } +scarlet-okhttp = { module = "com.tinder.scarlet:websocket-okhttp", version.ref = "scarlet" } +scarlet-coroutines = { module = "com.tinder.scarlet:stream-adapter-coroutines", version.ref = "scarlet" } +scarlet-moshi = { module = "com.tinder.scarlet:message-adapter-moshi", version.ref = "scarlet" } +scarlet-android = { module = "com.tinder.scarlet:lifecycle-android", version.ref = "scarlet" } +scarlet-mockwebserver = { module = "com.tinder.scarlet:websocket-mockwebserver", version.ref = "scarlet" } +scarlet-testUtils = { module = "com.tinder.scarlet:test-utils", version.ref = "scarlet" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } diff --git a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt index c31e85df4..af952d5a4 100644 --- a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt +++ b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt @@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -101,9 +100,7 @@ internal class NotifyEngine( } suspend fun setup() { - jsonRpcInteractor.wssConnectionState - .onEach { state -> handleWSSState(state) } - .filterIsInstance() + jsonRpcInteractor.onResubscribe .onEach { supervisorScope { launch(Dispatchers.IO) { diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt index ca5333dcb..eee6271ca 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt @@ -581,6 +581,11 @@ class SignProtocol(private val koinApp: KoinApplication = wcKoinApp) : SignInter onDelegate(Sign.Model.ConnectionState(true)) } + atomicBoolean?.get() == false && connectionState is WSSConnectionState.Disconnected.ConnectionFailed -> { + atomicBoolean?.set(false) + onDelegate(Sign.Model.ConnectionState(false, Sign.Model.ConnectionState.Reason.ConnectionFailed(connectionState.throwable))) + } + else -> Unit } }.launchIn(scope) diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt index 82ea66e9c..834168dd9 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt @@ -215,42 +215,25 @@ internal class SignEngine( } private fun handleRelayRequestsAndResponses() { - scope.launch { - supervisorScope { - launch(Dispatchers.IO) { - resubscribeToSession() - resubscribeToPendingAuthenticateTopics() - } - } - - if (jsonRpcRequestsJob == null) { - jsonRpcRequestsJob = collectJsonRpcRequests() - } - - if (jsonRpcResponsesJob == null) { - jsonRpcResponsesJob = collectJsonRpcResponses() - } - } + jsonRpcInteractor.onResubscribe + .onEach { + scope.launch { + supervisorScope { + launch(Dispatchers.IO) { + resubscribeToSession() + resubscribeToPendingAuthenticateTopics() + } + } + if (jsonRpcRequestsJob == null) { + jsonRpcRequestsJob = collectJsonRpcRequests() + } -// jsonRpcInteractor.wssConnectionState -// .filterIsInstance() -// .onEach { -// supervisorScope { -// launch(Dispatchers.IO) { -// resubscribeToSession() -// resubscribeToPendingAuthenticateTopics() -// } -// } -// -// if (jsonRpcRequestsJob == null) { -// jsonRpcRequestsJob = collectJsonRpcRequests() -// } -// -// if (jsonRpcResponsesJob == null) { -// jsonRpcResponsesJob = collectJsonRpcResponses() -// } -// }.launchIn(scope) + if (jsonRpcResponsesJob == null) { + jsonRpcResponsesJob = collectJsonRpcResponses() + } + } + }.launchIn(scope) } private fun handleLinkModeResponses() { diff --git a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt index 27f84ec39..0672c98d7 100644 --- a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt +++ b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt @@ -74,13 +74,14 @@ class Web3WalletApplication : Application() { metaData = appMetaData, onError = { error -> Firebase.crashlytics.recordException(error.throwable) - println(error.throwable.stackTraceToString()) + println("kobe: core init: ${error.throwable.stackTraceToString()}") scope.launch { connectionStateFlow.emit(ConnectionState.Error(error.throwable.message ?: "")) } } ) + mixPanel = MixpanelAPI.getInstance(this, CommonBuildConfig.MIX_PANEL, true).apply { identify(CoreClient.Push.clientId) people.set("\$name", EthAccountDelegate.ethAddress) diff --git a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/domain/WCDelegate.kt b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/domain/WCDelegate.kt index 2b6d3b75d..57d248966 100644 --- a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/domain/WCDelegate.kt +++ b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/domain/WCDelegate.kt @@ -43,7 +43,6 @@ object WCDelegate : Web3Wallet.WalletDelegate, CoreClient.CoreDelegate { } override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { - state.isAvailable scope.launch { _connectionState.emit(state) } diff --git a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/Web3WalletViewModel.kt b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/Web3WalletViewModel.kt index 5e8b4ccb6..4687327d2 100644 --- a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/Web3WalletViewModel.kt +++ b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/Web3WalletViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import com.walletconnect.android.Core +import com.walletconnect.android.internal.common.exception.InvalidProjectIdException +import com.walletconnect.android.internal.common.exception.ProjectIdDoesNotExistException import com.walletconnect.sample.wallet.domain.ISSUER import com.walletconnect.sample.wallet.domain.WCDelegate import com.walletconnect.sample.wallet.ui.state.ConnectionState @@ -68,7 +70,17 @@ class Web3WalletViewModel : ViewModel() { val connectionState = if (it.isAvailable) { ConnectionState.Ok } else { - ConnectionState.Error("No Internet connection, please check your internet connection and try again") + val message = when (it.reason) { + is Wallet.Model.ConnectionState.Reason.ConnectionFailed -> { + if ((it.reason as Wallet.Model.ConnectionState.Reason.ConnectionFailed).throwable is ProjectIdDoesNotExistException || + (it.reason as Wallet.Model.ConnectionState.Reason.ConnectionFailed).throwable is InvalidProjectIdException + ) "Invalid Project Id" else "Connection failed" + } + + else -> "Connection closed" + } + + ConnectionState.Error(message) } connectivityStateFlow.value = connectionState }.launchIn(viewModelScope) diff --git a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt index d5d18566c..d564d898d 100644 --- a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt +++ b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt @@ -108,17 +108,17 @@ fun WalletSampleHost( ) if (connectionState is ConnectionState.Error) { - ErrorBanner() + ErrorBanner((connectionState as ConnectionState.Error).message) } else if (connectionState is ConnectionState.Ok) { RestoredConnectionBanner() - } - if (isLoader) { - Loader(initMessage = "WalletConnect is pairing...", updateMessage = "Pairing is taking longer than usual, please try again...") - } + if (isLoader) { + Loader(initMessage = "WalletConnect is pairing...", updateMessage = "Pairing is taking longer than usual, please try again...") + } - if (isRequestLoader) { - Loader(initMessage = "Awaiting a request...", updateMessage = "It is taking longer than usual..") + if (isRequestLoader) { + Loader(initMessage = "Awaiting a request...", updateMessage = "It is taking longer than usual..") + } } Timer(web3walletViewModel) @@ -185,11 +185,11 @@ private fun BoxScope.Loader(initMessage: String, updateMessage: String) { } @Composable -private fun ErrorBanner() { +private fun ErrorBanner(message: String) { var shouldShow by remember { mutableStateOf(true) } LaunchedEffect(key1 = Unit) { - delay(2000) + delay(5000) shouldShow = false } @@ -208,7 +208,7 @@ private fun ErrorBanner() { colorFilter = ColorFilter.tint(color = Color.White) ) Spacer(modifier = Modifier.width(4.dp)) - Text(text = "Network connection lost", color = Color.White) + Text(text = "Network connection lost: $message", color = Color.White) } } } From c8c53b7e1e736336c996a041215231bd67da31e1 Mon Sep 17 00:00:00 2001 From: kubel Date: Thu, 29 Aug 2024 15:52:09 +0200 Subject: [PATCH 3/7] Add init event --- .../internal/common/di/CorePairingModule.kt | 3 ++ .../common/storage/events/EventsRepository.kt | 6 ++-- .../pairing/engine/domain/PairingEngine.kt | 30 +++++++++++++----- .../android/pulse/model/EventType.kt | 4 +++ .../pulse/model/properties/Properties.kt | 2 ++ .../android/pulse/model/properties/Props.kt | 3 +- .../android/sdk/storage/data/dao/Event.sq | 9 +++--- .../src/main/sqldelight/databases/12.db | Bin 0 -> 65536 bytes .../src/main/sqldelight/migration/11.sqm | 3 ++ 9 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 core/android/src/main/sqldelight/databases/12.db create mode 100644 core/android/src/main/sqldelight/migration/11.sqm diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CorePairingModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CorePairingModule.kt index 0c3fad1d6..70a73b8d8 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CorePairingModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CorePairingModule.kt @@ -15,8 +15,11 @@ fun corePairingModule(pairing: PairingInterface, pairingController: PairingContr pairingRepository = get(), jsonRpcInteractor = get(), logger = get(named(AndroidCommonDITags.LOGGER)), + insertTelemetryEventUseCase = get(), insertEventUseCase = get(), sendBatchEventUseCase = get(), + clientId = get(named(AndroidCommonDITags.CLIENT_ID)), + userAgent = get(named(AndroidCommonDITags.USER_AGENT)) ) } single { pairing } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/events/EventsRepository.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/events/EventsRepository.kt index 3463f721f..6d824430f 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/events/EventsRepository.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/events/EventsRepository.kt @@ -38,7 +38,8 @@ class EventsRepository( this.props.properties?.trace, this.props.properties?.correlationId, this.props.properties?.clientId, - this.props.properties?.direction + this.props.properties?.direction, + this.props.properties?.userAgent ) } } @@ -85,7 +86,8 @@ class EventsRepository( trace = trace, clientId = client_id, correlationId = correlation_id, - direction = direction + direction = direction, + userAgent = user_agent ) ) ) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt index b987d4fcb..c9c1ce1ed 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt @@ -39,6 +39,7 @@ import com.walletconnect.android.pairing.model.PairingParams import com.walletconnect.android.pairing.model.PairingRpc import com.walletconnect.android.pairing.model.mapper.toCore import com.walletconnect.android.pairing.model.pairingExpiry +import com.walletconnect.android.pulse.domain.InsertEventUseCase import com.walletconnect.android.pulse.domain.InsertTelemetryEventUseCase import com.walletconnect.android.pulse.domain.SendBatchEventUseCase import com.walletconnect.android.pulse.model.EventType @@ -82,8 +83,11 @@ internal class PairingEngine( private val crypto: KeyManagementRepository, private val jsonRpcInteractor: RelayJsonRpcInteractorInterface, private val pairingRepository: PairingStorageRepositoryInterface, - private val insertEventUseCase: InsertTelemetryEventUseCase, - private val sendBatchEventUseCase: SendBatchEventUseCase + private val insertTelemetryEventUseCase: InsertTelemetryEventUseCase, + private val insertEventUseCase: InsertEventUseCase, + private val sendBatchEventUseCase: SendBatchEventUseCase, + private val clientId: String, + private val userAgent: String ) { private var jsonRpcRequestsJob: Job? = null private val setOfRegisteredMethods: MutableSet = mutableSetOf() @@ -158,7 +162,7 @@ internal class PairingEngine( val trace: MutableList = mutableListOf() trace.add(Trace.Pairing.PAIRING_STARTED).also { logger.log("Pairing started") } val walletConnectUri: WalletConnectUri = Validator.validateWCUri(uri) ?: run { - scope.launch { supervisorScope { insertEventUseCase(Props(type = EventType.Error.MALFORMED_PAIRING_URI, properties = Properties(trace = trace))) } } + scope.launch { supervisorScope { insertTelemetryEventUseCase(Props(type = EventType.Error.MALFORMED_PAIRING_URI, properties = Properties(trace = trace))) } } return onFailure(MalformedWalletConnectUri(MALFORMED_PAIRING_URI_MESSAGE)) } trace.add(Trace.Pairing.PAIRING_URI_VALIDATION_SUCCESS) @@ -167,7 +171,7 @@ internal class PairingEngine( val symmetricKey = walletConnectUri.symKey try { if (walletConnectUri.expiry?.isExpired() == true) { - scope.launch { supervisorScope { insertEventUseCase(Props(type = EventType.Error.PAIRING_URI_EXPIRED, properties = Properties(trace = trace, topic = pairingTopic.value))) } } + scope.launch { supervisorScope { insertTelemetryEventUseCase(Props(type = EventType.Error.PAIRING_URI_EXPIRED, properties = Properties(trace = trace, topic = pairingTopic.value))) } } .also { logger.error("Pairing URI expired: $pairingTopic") } return onFailure(ExpiredPairingURIException("Pairing URI expired: $pairingTopic")) } @@ -176,7 +180,7 @@ internal class PairingEngine( val localPairing = pairingRepository.getPairingOrNullByTopic(pairingTopic) trace.add(Trace.Pairing.EXISTING_PAIRING) if (!localPairing!!.isNotExpired()) { - scope.launch { supervisorScope { insertEventUseCase(Props(type = EventType.Error.PAIRING_EXPIRED, properties = Properties(trace = trace, topic = pairingTopic.value))) } } + scope.launch { supervisorScope { insertTelemetryEventUseCase(Props(type = EventType.Error.PAIRING_EXPIRED, properties = Properties(trace = trace, topic = pairingTopic.value))) } } .also { logger.error("Pairing expired: $pairingTopic") } return onFailure(ExpiredPairingException("Pairing expired: ${pairingTopic.value}")) } @@ -200,7 +204,7 @@ internal class PairingEngine( }, onFailure = { error -> scope.launch { supervisorScope { - insertEventUseCase(Props(type = EventType.Error.PAIRING_SUBSCRIPTION_FAILURE, properties = Properties(trace = trace, topic = pairingTopic.value))) + insertTelemetryEventUseCase(Props(type = EventType.Error.PAIRING_SUBSCRIPTION_FAILURE, properties = Properties(trace = trace, topic = pairingTopic.value))) } }.also { logger.error("Subscribe pairing topic error: $pairingTopic, error: $error") } onFailure(error) @@ -209,9 +213,18 @@ internal class PairingEngine( } catch (e: Exception) { logger.error("Subscribe pairing topic error: $pairingTopic, error: $e") if (e is NoRelayConnectionException) - scope.launch { supervisorScope { insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION, properties = Properties(trace = trace, topic = pairingTopic.value))) } } + scope.launch { supervisorScope { insertTelemetryEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION, properties = Properties(trace = trace, topic = pairingTopic.value))) } } if (e is NoInternetConnectionException) - scope.launch { supervisorScope { insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION, properties = Properties(trace = trace, topic = pairingTopic.value))) } } + scope.launch { + supervisorScope { + insertTelemetryEventUseCase( + Props( + type = EventType.Error.NO_INTERNET_CONNECTION, + properties = Properties(trace = trace, topic = pairingTopic.value) + ) + ) + } + } runCatching { crypto.removeKeys(pairingTopic.value) }.onFailure { logger.error("Remove keys error: $pairingTopic, error: $it") } jsonRpcInteractor.unsubscribe(pairingTopic) onFailure(e) @@ -286,6 +299,7 @@ internal class PairingEngine( scope.launch { supervisorScope { try { + insertEventUseCase(Props(event = EventType.INIT, properties = Properties(clientId = clientId, userAgent = userAgent))) sendBatchEventUseCase() } catch (e: Exception) { logger.error("Error when sending events: $e") diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/EventType.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/EventType.kt index 41e0c9a87..1ec63f6f5 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/EventType.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/EventType.kt @@ -3,9 +3,13 @@ package com.walletconnect.android.pulse.model object EventType { @get:JvmSynthetic const val ERROR: String = "ERROR" + @get:JvmSynthetic const val SUCCESS: String = "SUCCESS" + @get:JvmSynthetic + const val INIT: String = "INIT" + @get:JvmSynthetic const val TRACK: String = "TRACE" diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Properties.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Properties.kt index 0284da3d4..fceb0c53e 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Properties.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Properties.kt @@ -27,4 +27,6 @@ data class Properties( val clientId: String? = null, @Json(name = "direction") val direction: String? = null, + @Json(name = "user_agent") + val userAgent: String? = null, ) \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Props.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Props.kt index 0bfb09d8e..d7d006144 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Props.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Props.kt @@ -2,12 +2,13 @@ package com.walletconnect.android.pulse.model.properties import com.squareup.moshi.Json import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.utils.Empty data class Props( @Json(name = "event") val event: String = EventType.ERROR, @Json(name = "type") - val type: String, + val type: String = String.Empty, @Json(name = "properties") val properties: Properties? = null ) \ No newline at end of file diff --git a/core/android/src/main/sqldelight/com/walletconnect/android/sdk/storage/data/dao/Event.sq b/core/android/src/main/sqldelight/com/walletconnect/android/sdk/storage/data/dao/Event.sq index 9e573c2a4..b94f1a632 100644 --- a/core/android/src/main/sqldelight/com/walletconnect/android/sdk/storage/data/dao/Event.sq +++ b/core/android/src/main/sqldelight/com/walletconnect/android/sdk/storage/data/dao/Event.sq @@ -11,15 +11,16 @@ CREATE TABLE EventDao( trace TEXT AS List, correlation_id INTEGER, client_id TEXT, - direction TEXT + direction TEXT, + user_agent TEXT ); insertOrAbort: -INSERT OR ABORT INTO EventDao(event_id, bundle_id, timestamp, event_name, type, topic, trace, correlation_id, client_id, direction) -VALUES (?,?,?,?,?,?, ?, ?, ?, ?); +INSERT OR ABORT INTO EventDao(event_id, bundle_id, timestamp, event_name, type, topic, trace, correlation_id, client_id, direction, user_agent) +VALUES (?,?,?,?,?,?, ?, ?, ?, ?, ?); getAllEventsWithLimitAndOffset: -SELECT event_id, bundle_id, timestamp, event_name, type, topic, trace, correlation_id, client_id, direction +SELECT event_id, bundle_id, timestamp, event_name, type, topic, trace, correlation_id, client_id, direction, user_agent FROM EventDao ed LIMIT ? OFFSET ?; diff --git a/core/android/src/main/sqldelight/databases/12.db b/core/android/src/main/sqldelight/databases/12.db new file mode 100644 index 0000000000000000000000000000000000000000..21a315fd6e10f466acc10f74c675df8c407ddf52 GIT binary patch literal 65536 zcmeI&OK#gn7{GBU56O}vCqNfm7%9f^A`z0t6@upB}ZEotEe{zuj$#Nuju375b%mqP^c~J!>6_gQNCd^XR46X}uK9Zs&P>Umv&E z+V6<{=lb2<-F+Ax61YQUi%#oBM|Ahwhuzjfy*wW(Z+vyy`GY;#x5nnsa|73Q`|(-Q zTJ)^IifZHU9C<3O?LY>v-JYLTrF_$}19c`dE=1Sss~Ebz)i*t9OWkrxvEBNv+1>4k zMz#LM?UK=4FBX*3ljr`MzFwkDYZSQAzB$1(8kY%CS6U36B<#=U#R%aaK9C+VqI{49bHuBT3vvmCGxXI5Y7?y21L+UUZ}k(b)Jb+7YnYmoHGYW>2ml#Pvz z;@d_PR=X#iKm|(r)2-@UK-2P%NAs}}tm1E(q%);!2w~Z_J92_{FHOLAL=9PmoILVjk|Y?@2*GZ z+>?Q|ttZvw^XA-HlF}IIQ&xmZa+PKlUwB0+d_C#go*Jf)qe|-dsVG~|H7VzE^l;QO zeVE*E4ys&DJqt^dH5{6K<(yu%6e8R;2X60borq2lkBR&9u{{*wdF$2s*3FXf^lny; z6HBv^Y?Ur**UDjfFAk$z#FNs&=(w-!9XVduy*`(jh01uv1%cn4az+$ly~>{k_q)jHFYmtNzN&uepmB&Bf#GHg4$ zKxIdAJaT${8MciEZ!*;49|nOM=#|bI440ye>*Mc9?}B=c3(|GOS+>FJ*9tQT=n$0-N=`osV6Tdh~lc&Ya!|EU4FRF3jqWW zKmY**5I_I{1Q0*~0R(a-!2N%&(n&A~Ab28m z2q1s}0tg_000IagfIwaZc>bT4OcDnI2q1s}0tg_000IagfB*v70zCiEwxI?A1Q0*~ z0R#|0009ILKmdWf2=M$rFPS6`1Q0*~0R#|0009ILKmY**vIV&R&$gik0R#|0009IL zKmY**5I_Kdya@3B|GZ?9I1oSp0R#|0009ILKmY**5Xctb`G2+zH3%Sp00IagfB*sr zAb Date: Fri, 30 Aug 2024 15:40:08 +0200 Subject: [PATCH 4/7] Add retry connection flow --- .../ConditionalExponentialBackoffStrategy.kt | 1 - .../connection/DefaultConnectionLifecycle.kt | 5 +- .../connection/ManualConnectionLifecycle.kt | 1 + .../internal/common/di/CoreJsonRpcModule.kt | 10 +- .../domain/relay/RelayJsonRpcInteractor.kt | 242 +++++++----------- .../android/pairing/client/PairingProtocol.kt | 54 +--- .../pairing/engine/domain/PairingEngine.kt | 1 - .../android/relay/RelayClient.kt | 17 +- .../android/relay/RelayConnectionInterface.kt | 2 + .../foundation/network/BaseRelayClient.kt | 192 ++++++++++++-- .../network}/ConnectionLifecycle.kt | 2 +- gradle/libs.versions.toml | 16 +- .../notify/engine/NotifyEngine.kt | 5 +- .../domain/WatchSubscriptionsUseCase.kt | 23 +- .../walletconnect/sign/client/SignProtocol.kt | 10 +- .../sign/engine/domain/SignEngine.kt | 2 - .../dapp/ui/routes/host/DappSampleHost.kt | 39 +-- .../sample/wallet/Web3WalletApplication.kt | 2 +- 18 files changed, 330 insertions(+), 294 deletions(-) rename {core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection => foundation/src/main/kotlin/com/walletconnect/foundation/network}/ConnectionLifecycle.kt (68%) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt index 72a7978d9..2353efb61 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/ConditionalExponentialBackoffStrategy.kt @@ -15,7 +15,6 @@ class ConditionalExponentialBackoffStrategy( override var shouldBackoff: Boolean = false fun shouldBackoff(shouldBackoff: Boolean) { - println("kobe:be Sending shouldBackoff: $shouldBackoff") this.shouldBackoff = shouldBackoff } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt index 48b80f918..d8b4c86b6 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/DefaultConnectionLifecycle.kt @@ -8,6 +8,7 @@ import android.os.Bundle import com.tinder.scarlet.Lifecycle import com.tinder.scarlet.ShutdownReason import com.tinder.scarlet.lifecycle.LifecycleRegistry +import com.walletconnect.foundation.network.ConnectionLifecycle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -34,7 +35,6 @@ internal class DefaultConnectionLifecycle( } override fun reconnect() { - println("kobe: reconnect()") lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason()) lifecycleRegistry.onNext(Lifecycle.State.Started) } @@ -44,13 +44,11 @@ internal class DefaultConnectionLifecycle( var job: Job? = null override fun onActivityPaused(activity: Activity) { - println("kobe: pause") isResumed = false job = scope.launch { delay(TimeUnit.SECONDS.toMillis(30)) if (!isResumed) { - println("kobe: onPaused; disconnect()") lifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason(ShutdownReason(1000, "App is paused"))) job = null _onResume.value = false @@ -59,7 +57,6 @@ internal class DefaultConnectionLifecycle( } override fun onActivityResumed(activity: Activity) { - println("kobe: resume") isResumed = true if (job?.isActive == true) { diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt index 2d3076761..d331c965b 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ManualConnectionLifecycle.kt @@ -4,6 +4,7 @@ package com.walletconnect.android.internal.common.connection import com.tinder.scarlet.Lifecycle import com.tinder.scarlet.lifecycle.LifecycleRegistry +import com.walletconnect.foundation.network.ConnectionLifecycle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt index c798938ba..f08944b30 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt @@ -1,7 +1,6 @@ package com.walletconnect.android.internal.common.di import com.squareup.moshi.Moshi -import com.walletconnect.android.internal.common.connection.ConnectionLifecycle import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer @@ -13,6 +12,7 @@ import com.walletconnect.android.internal.common.model.type.SerializableJsonRpc import com.walletconnect.android.pairing.model.PairingJsonRpcMethod import com.walletconnect.android.pairing.model.PairingRpc import com.walletconnect.android.relay.ConnectionType +import com.walletconnect.foundation.network.ConnectionLifecycle import com.walletconnect.utils.JsonAdapterEntry import com.walletconnect.utils.addDeserializerEntry import com.walletconnect.utils.addSerializerEntry @@ -23,13 +23,6 @@ import org.koin.dsl.module import kotlin.reflect.KClass -fun Scope.getConnectionLifecycle(connectionType: ConnectionType): ConnectionLifecycle = - if (connectionType == ConnectionType.MANUAL) { - get(named(AndroidCommonDITags.MANUAL_CONNECTION_LIFECYCLE)) - } else { - get(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) - } - @JvmSynthetic fun coreJsonRpcModule(connectionType: ConnectionType) = module { @@ -40,7 +33,6 @@ fun coreJsonRpcModule(connectionType: ConnectionType) = module { jsonRpcHistory = get(), pushMessageStorage = get(), logger = get(named(AndroidCommonDITags.LOGGER)), - connectionLifecycle = getConnectionLifecycle(connectionType), backoffStrategy = get() ) } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt index baa03ec72..52f0e8e27 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt @@ -2,12 +2,10 @@ package com.walletconnect.android.internal.common.json_rpc.domain.relay import com.walletconnect.android.internal.common.ConditionalExponentialBackoffStrategy import com.walletconnect.android.internal.common.JsonRpcResponse -import com.walletconnect.android.internal.common.connection.ConnectionLifecycle import com.walletconnect.android.internal.common.crypto.codec.Codec import com.walletconnect.android.internal.common.crypto.sha256 import com.walletconnect.android.internal.common.exception.NoConnectivityException import com.walletconnect.android.internal.common.exception.NoInternetConnectionException -import com.walletconnect.android.internal.common.exception.NoRelayConnectionException import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer import com.walletconnect.android.internal.common.json_rpc.model.toRelay import com.walletconnect.android.internal.common.json_rpc.model.toWCRequest @@ -43,11 +41,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope @@ -67,7 +62,6 @@ internal class RelayJsonRpcInteractor( private val jsonRpcHistory: JsonRpcHistory, private val pushMessageStorage: PushMessagesRepository, private val logger: Logger, - private val connectionLifecycle: ConnectionLifecycle, private val backoffStrategy: ConditionalExponentialBackoffStrategy ) : RelayJsonRpcInteractorInterface { private val serializer: JsonRpcSerializer get() = wcKoinApp.koin.get() @@ -83,11 +77,7 @@ internal class RelayJsonRpcInteractor( override val wssConnectionState: StateFlow get() = relay.wssConnectionState private var subscriptions = ObservableMap(mutableMapOf()) { newMap -> backoffStrategy.shouldBackoff(newMap.isNotEmpty()) } - override val onResubscribe: Flow - get() = merge( - connectionLifecycle.onResume.filter { isResumed -> isResumed != null && isResumed }, - relay.wssConnectionState.filterIsInstance(WSSConnectionState.Connected::class) - ) + override val onResubscribe: Flow = relay.onResubscribe init { manageSubscriptions() @@ -99,47 +89,6 @@ internal class RelayJsonRpcInteractor( } } - private fun connectAndCallRelay(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { - if (relay.wssConnectionState.value is WSSConnectionState.Disconnected) { - connectionLifecycle.reconnect() - //todo: timeout(?) + check how many times we try to re-connect - scope.launch { - supervisorScope { - relay.wssConnectionState - .filter { state -> - when (state) { - is WSSConnectionState.Disconnected.ConnectionClosed -> state.message != null - else -> true - } - } - .first { state -> - println("kobe: interactor state: $state") - when (state) { - is WSSConnectionState.Connected -> { - println("kobe: action") - onConnected() - } - - is WSSConnectionState.Disconnected.ConnectionFailed -> { - println("kobe: error failed: ${state.throwable}") - onFailure(NoRelayConnectionException(state.throwable.message)) - } - - is WSSConnectionState.Disconnected.ConnectionClosed -> { - println("kobe: error closed: $state") - onFailure(NoRelayConnectionException(state.message)) - } - } - true - } - } - } - } else if (relay.wssConnectionState.value is WSSConnectionState.Connected) { - println("kobe: already connected") - onConnected() - } - } - override fun publishJsonRpcRequest( topic: Topic, params: IrnParams, @@ -155,31 +104,26 @@ internal class RelayJsonRpcInteractor( return onFailure(e) } - connectAndCallRelay(onConnected = { - try { - val requestJson = serializer.serialize(payload) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Request Params") - - println("kobe: Request: $requestJson") - - if (jsonRpcHistory.setRequest(payload.id, topic, payload.method, requestJson, TransportType.RELAY)) { - val encryptedRequest = chaChaPolyCodec.encrypt(topic, requestJson, envelopeType, participants) - val encryptedRequestString = Base64.toBase64String(encryptedRequest) + try { + val requestJson = serializer.serialize(payload) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Request Params") + if (jsonRpcHistory.setRequest(payload.id, topic, payload.method, requestJson, TransportType.RELAY)) { + val encryptedRequest = chaChaPolyCodec.encrypt(topic, requestJson, envelopeType, participants) + val encryptedRequestString = Base64.toBase64String(encryptedRequest) - relay.publish(topic.value, encryptedRequestString, params.toRelay()) { result -> - result.fold( - onSuccess = { onSuccess() }, - onFailure = { error -> - logger.error("JsonRpcInteractor: Cannot send the request, error: $error") - onFailure(Throwable("Publish error: ${error.message}")) - } - ) - } + relay.publish(topic.value, encryptedRequestString, params.toRelay()) { result -> + result.fold( + onSuccess = { onSuccess() }, + onFailure = { error -> + logger.error("JsonRpcInteractor: Cannot send the request, error: $error") + onFailure(Throwable("Publish error: ${error.message}")) + } + ) } - } catch (e: Exception) { - logger.error("JsonRpcInteractor: Cannot send the request, exception: $e") - onFailure(Throwable("Publish Request Error: $e")) } - }, onFailure = { error -> onFailure(error) }) + } catch (e: Exception) { + logger.error("JsonRpcInteractor: Cannot send the request, exception: $e") + onFailure(Throwable("Publish Request Error: $e")) + } } override fun publishJsonRpcResponse( @@ -197,31 +141,26 @@ internal class RelayJsonRpcInteractor( return onFailure(e) } - connectAndCallRelay(onConnected = { - try { - val responseJson = serializer.serialize(response) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Response Params") - val encryptedResponse = chaChaPolyCodec.encrypt(topic, responseJson, envelopeType, participants) - val encryptedResponseString = Base64.toBase64String(encryptedResponse) - - println("kobe: Response: $responseJson") - - relay.publish(topic.value, encryptedResponseString, params.toRelay()) { result -> - result.fold( - onSuccess = { - jsonRpcHistory.updateRequestWithResponse(response.id, responseJson) - onSuccess() - }, - onFailure = { error -> - logger.error("JsonRpcInteractor: Cannot send the response, error: $error") - onFailure(Throwable("Publish error: ${error.message}")) - } - ) - } - } catch (e: Exception) { - logger.error("JsonRpcInteractor: Cannot send the response, exception: $e") - onFailure(Throwable("Publish Response Error: $e")) + try { + val responseJson = serializer.serialize(response) ?: throw IllegalStateException("RelayJsonRpcInteractor: Unknown Response Params") + val encryptedResponse = chaChaPolyCodec.encrypt(topic, responseJson, envelopeType, participants) + val encryptedResponseString = Base64.toBase64String(encryptedResponse) + relay.publish(topic.value, encryptedResponseString, params.toRelay()) { result -> + result.fold( + onSuccess = { + jsonRpcHistory.updateRequestWithResponse(response.id, responseJson) + onSuccess() + }, + onFailure = { error -> + logger.error("JsonRpcInteractor: Cannot send the response, error: $error") + onFailure(Throwable("Publish error: ${error.message}")) + } + ) } - }, onFailure = { error -> onFailure(error) }) + } catch (e: Exception) { + logger.error("JsonRpcInteractor: Cannot send the response, exception: $e") + onFailure(Throwable("Publish Response Error: $e")) + } } override fun subscribe(topic: Topic, onSuccess: (Topic) -> Unit, onFailure: (Throwable) -> Unit) { @@ -231,25 +170,23 @@ internal class RelayJsonRpcInteractor( return onFailure(e) } - connectAndCallRelay(onConnected = { - try { - relay.subscribe(topic.value) { result -> - result.fold( - onSuccess = { acknowledgement -> - subscriptions[topic.value] = acknowledgement.result - onSuccess(topic) - }, - onFailure = { error -> - logger.error("Subscribe to topic error: $topic error: $error") - onFailure(Throwable("Subscribe error: ${error.message}")) - } - ) - } - } catch (e: Exception) { - logger.error("Subscribe to topic error: $topic error: $e") - onFailure(Throwable("Subscribe error: ${e.message}")) + try { + relay.subscribe(topic.value) { result -> + result.fold( + onSuccess = { acknowledgement -> + subscriptions[topic.value] = acknowledgement.result + onSuccess(topic) + }, + onFailure = { error -> + logger.error("Subscribe to topic error: $topic error: $error") + onFailure(Throwable("Subscribe error: ${error.message}")) + } + ) } - }, onFailure = { error -> onFailure(error) }) + } catch (e: Exception) { + logger.error("Subscribe to topic error: $topic error: $e") + onFailure(Throwable("Subscribe error: ${e.message}")) + } } override fun batchSubscribe(topics: List, onSuccess: (List) -> Unit, onFailure: (Throwable) -> Unit) { @@ -260,25 +197,23 @@ internal class RelayJsonRpcInteractor( } if (topics.isNotEmpty()) { - connectAndCallRelay(onConnected = { - try { - relay.batchSubscribe(topics) { result -> - result.fold( - onSuccess = { acknowledgement -> - subscriptions.plusAssign(topics.zip(acknowledgement.result).toMap()) - onSuccess(topics) - }, - onFailure = { error -> - logger.error("Batch subscribe to topics error: $topics error: $error") - onFailure(Throwable("Batch subscribe error: ${error.message}")) - } - ) - } - } catch (e: Exception) { - logger.error("Batch subscribe to topics error: $topics error: $e") - onFailure(Throwable("Batch subscribe error: ${e.message}")) + try { + relay.batchSubscribe(topics) { result -> + result.fold( + onSuccess = { acknowledgement -> + subscriptions.plusAssign(topics.zip(acknowledgement.result).toMap()) + onSuccess(topics) + }, + onFailure = { error -> + logger.error("Batch subscribe to topics error: $topics error: $error") + onFailure(Throwable("Batch subscribe error: ${error.message}")) + } + ) } - }, onFailure = { error -> onFailure(error) }) + } catch (e: Exception) { + logger.error("Batch subscribe to topics error: $topics error: $e") + onFailure(Throwable("Batch subscribe error: ${e.message}")) + } } } @@ -290,27 +225,25 @@ internal class RelayJsonRpcInteractor( } if (subscriptions.contains(topic.value)) { - connectAndCallRelay(onConnected = { - val subscriptionId = SubscriptionId(subscriptions[topic.value].toString()) - relay.unsubscribe(topic.value, subscriptionId.id) { result -> - result.fold( - onSuccess = { - scope.launch { - supervisorScope { - jsonRpcHistory.deleteRecordsByTopic(topic) - subscriptions.remove(topic.value) - pushMessageStorage.deletePushMessagesByTopic(topic.value) - onSuccess() - } + val subscriptionId = SubscriptionId(subscriptions[topic.value].toString()) + relay.unsubscribe(topic.value, subscriptionId.id) { result -> + result.fold( + onSuccess = { + scope.launch { + supervisorScope { + jsonRpcHistory.deleteRecordsByTopic(topic) + subscriptions.remove(topic.value) + pushMessageStorage.deletePushMessagesByTopic(topic.value) + onSuccess() } - }, - onFailure = { error -> - logger.error("Unsubscribe to topic: $topic error: $error") - onFailure(Throwable("Unsubscribe error: ${error.message}")) } - ) - } - }, onFailure = { error -> onFailure(error) }) + }, + onFailure = { error -> + logger.error("Unsubscribe to topic: $topic error: $error") + onFailure(Throwable("Unsubscribe error: ${error.message}")) + } + ) + } } } @@ -425,9 +358,6 @@ internal class RelayJsonRpcInteractor( storePushRequestsIfEnabled(relayRequest, topic) Subscription(decryptMessage(topic, relayRequest), relayRequest.message, topic, relayRequest.publishedAt, relayRequest.attestation) }.collect { subscription -> - - println("kobe: Message: ${subscription.decryptedMessage}") - if (subscription.decryptedMessage.isNotEmpty()) { try { manageSubscriptions(subscription) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt index 401b009e7..9f53e079b 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/client/PairingProtocol.kt @@ -12,9 +12,7 @@ import com.walletconnect.android.pairing.model.mapper.toCore import com.walletconnect.android.pulse.domain.InsertTelemetryEventUseCase import com.walletconnect.android.relay.RelayConnectionInterface import com.walletconnect.foundation.util.Logger -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import org.koin.core.KoinApplication internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) : PairingInterface { @@ -70,24 +68,14 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) onError: (Core.Model.Error) -> Unit, ) { checkEngineInitialization() - - scope.launch(Dispatchers.IO) { -// awaitConnection( -// { - try { - pairingEngine.pair( - uri = pair.uri, - onSuccess = { onSuccess(pair) }, - onFailure = { error -> onError(Core.Model.Error(Throwable("Pairing error: ${error.message}"))) } - ) - } catch (e: Exception) { - onError(Core.Model.Error(e)) - } -// }, -// { throwable -> -// logger.error(throwable) -// onError(Core.Model.Error(Throwable("Pairing error: ${throwable.message}"))) -// }) + try { + pairingEngine.pair( + uri = pair.uri, + onSuccess = { onSuccess(pair) }, + onFailure = { error -> onError(Core.Model.Error(Throwable("Pairing error: ${error.message}"))) } + ) + } catch (e: Exception) { + onError(Core.Model.Error(e)) } } @@ -144,32 +132,6 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) } } -// private suspend fun awaitConnection(onConnection: () -> Unit, errorLambda: (Throwable) -> Unit = {}) { -// try { -// withTimeout(60000) { -// while (true) { -// if (relayClient.isNetworkAvailable.value != null) { -// if (relayClient.isNetworkAvailable.value == true) { -// if (relayClient.wssConnectionState.value is WSSConnectionState.Connected) { -// onConnection() -// return@withTimeout -// } -// //todo: return an error after earlier - after tries ? -// } else { -// insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION)) -// errorLambda(Throwable("No internet connection")) -// return@withTimeout -// } -// } -// delay(100) -// } -// } -// } catch (e: Exception) { -// insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION)) -// errorLambda(Throwable("Failed to connect: ${e.message}")) -// } -// } - @Throws(IllegalStateException::class) private fun checkEngineInitialization() { check(::pairingEngine.isInitialized) { diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt index c9c1ce1ed..aec626150 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/engine/domain/PairingEngine.kt @@ -326,7 +326,6 @@ internal class PairingEngine( private suspend fun sendBatchSubscribeForPairings() { try { val pairingTopics = pairingRepository.getListOfPairings().filter { pairing -> pairing.isNotExpired() }.map { pairing -> pairing.topic.value } - println("kobe: re-subscribing to pairing topics: $pairingTopics") jsonRpcInteractor.batchSubscribe(pairingTopics) { error -> scope.launch { internalErrorFlow.emit(SDKError(error)) } } } catch (e: Exception) { scope.launch { internalErrorFlow.emit(SDKError(e)) } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt index 1e7771867..9b8909ff1 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt @@ -4,6 +4,7 @@ package com.walletconnect.android.relay import com.walletconnect.android.Core import com.walletconnect.android.internal.common.connection.ConnectivityState +import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle import com.walletconnect.android.internal.common.di.AndroidCommonDITags import com.walletconnect.android.internal.common.exception.WRONG_CONNECTION_TYPE @@ -12,10 +13,14 @@ import com.walletconnect.android.internal.common.wcKoinApp import com.walletconnect.android.utils.toWalletConnectException import com.walletconnect.foundation.network.BaseRelayClient import com.walletconnect.foundation.network.model.Relay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope @@ -24,18 +29,24 @@ import org.koin.core.qualifier.named class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayClient(), RelayConnectionInterface { private val manualConnection: ManualConnectionLifecycle by lazy { koinApp.koin.get(named(AndroidCommonDITags.MANUAL_CONNECTION_LIFECYCLE)) } + private val defaultConnection: DefaultConnectionLifecycle by lazy { koinApp.koin.get(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) } private val networkState: ConnectivityState by lazy { koinApp.koin.get(named(AndroidCommonDITags.CONNECTIVITY_STATE)) } override val isNetworkAvailable: StateFlow by lazy { networkState.isAvailable } - private val _wssConnectionState: MutableStateFlow = MutableStateFlow(WSSConnectionState.Disconnected.ConnectionClosed()) override val wssConnectionState: StateFlow = _wssConnectionState private lateinit var connectionType: ConnectionType + override val onResubscribe: Flow + get() = merge( + connectionLifecycle.onResume.filter { isResumed -> isResumed != null && isResumed }, + wssConnectionState.filterIsInstance(WSSConnectionState.Connected::class) + ) @JvmSynthetic fun initialize(connectionType: ConnectionType, onError: (Throwable) -> Unit) { this.connectionType = connectionType logger = koinApp.koin.get(named(AndroidCommonDITags.LOGGER)) relayService = koinApp.koin.get(named(AndroidCommonDITags.RELAY_SERVICE)) + connectionLifecycle = if (connectionType == ConnectionType.MANUAL) manualConnection else defaultConnection collectConnectionInitializationErrors { error -> onError(error) } monitorConnectionState() @@ -71,8 +82,8 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Connected -> _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) - event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Disconnected.ConnectionClosed -> - _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) +// event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Disconnected.ConnectionClosed -> +// _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) event is Relay.Model.Event.OnConnectionClosed && _wssConnectionState.value is WSSConnectionState.Connected -> _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionClosed("Connection closed: ${event.shutdownReason.reason} ${event.shutdownReason.code}") diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayConnectionInterface.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayConnectionInterface.kt index c3f13d6b9..0a6606c63 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayConnectionInterface.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayConnectionInterface.kt @@ -2,11 +2,13 @@ package com.walletconnect.android.relay import com.walletconnect.android.Core import com.walletconnect.foundation.network.RelayInterface +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface RelayConnectionInterface : RelayInterface { val wssConnectionState: StateFlow val isNetworkAvailable: StateFlow + val onResubscribe: Flow @Deprecated("This has become deprecate in favor of the onError returning Core.Model.Error", ReplaceWith("this.connect(onErrorModel)")) fun connect(onErrorModel: (Core.Model.Error) -> Unit = {}, onError: (String) -> Unit) diff --git a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt index 2d4c487f5..6408069fd 100644 --- a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt +++ b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt @@ -1,5 +1,6 @@ package com.walletconnect.foundation.network +import com.tinder.scarlet.WebSocket import com.walletconnect.foundation.common.model.SubscriptionId import com.walletconnect.foundation.common.model.Topic import com.walletconnect.foundation.common.model.Ttl @@ -12,11 +13,13 @@ import com.walletconnect.foundation.network.model.RelayDTO import com.walletconnect.foundation.util.Logger import com.walletconnect.foundation.util.scope import com.walletconnect.util.generateClientToServerId +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch @@ -28,18 +31,30 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.take import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withTimeout import org.koin.core.KoinApplication +sealed class ConnectionState { + data object Open : ConnectionState() + data class Closed(val throwable: Throwable) : ConnectionState() + data object Idle : ConnectionState() +} + @OptIn(ExperimentalCoroutinesApi::class) abstract class BaseRelayClient : RelayInterface { private var foundationKoinApp: KoinApplication = KoinApplication.init() lateinit var relayService: RelayService + lateinit var connectionLifecycle: ConnectionLifecycle protected var logger: Logger private val resultState: MutableSharedFlow = MutableSharedFlow() + private var connectionState: MutableStateFlow = MutableStateFlow(ConnectionState.Idle) + private var unAckedTopics: MutableList = mutableListOf() + private var isConnecting: Boolean = false + private var retryCount: Int = 0 override var isLoggingEnabled: Boolean = false init { @@ -73,10 +88,17 @@ abstract class BaseRelayClient : RelayInterface { override val eventsFlow: SharedFlow by lazy { relayService .observeWebSocketEvent() + .onEach { event -> + if (event is WebSocket.Event.OnConnectionOpened<*>) { + connectionState.value = ConnectionState.Open + } else if (event is WebSocket.Event.OnConnectionClosed || event is WebSocket.Event.OnConnectionFailed) { + connectionState.value = ConnectionState.Closed(Throwable(getErrorMessage(event))) + } + } .map { event -> -// if (isLoggingEnabled) { - println("kobe: Event: $event") -// } + if (isLoggingEnabled) { + println("Event: $event") + } event.toRelayEvent() } .shareIn(scope, SharingStarted.Lazily, REPLAY) @@ -96,12 +118,16 @@ abstract class BaseRelayClient : RelayInterface { id: Long?, onResult: (Result) -> Unit, ) { - val (tag, ttl, prompt) = params - val publishParams = RelayDTO.Publish.Request.Params(Topic(topic), message, Ttl(ttl), tag, prompt) - val publishRequest = RelayDTO.Publish.Request(id = id ?: generateClientToServerId(), params = publishParams) - - observePublishResult(publishRequest.id, onResult) - relayService.publishRequest(publishRequest) + connectAndCallRelay( + onConnected = { + val (tag, ttl, prompt) = params + val publishParams = RelayDTO.Publish.Request.Params(Topic(topic), message, Ttl(ttl), tag, prompt) + val publishRequest = RelayDTO.Publish.Request(id = id ?: generateClientToServerId(), params = publishParams) + observePublishResult(publishRequest.id, onResult) + relayService.publishRequest(publishRequest) + }, + onFailure = { onResult(Result.failure(it)) } + ) } private fun observePublishResult(id: Long, onResult: (Result) -> Unit) { @@ -131,14 +157,17 @@ abstract class BaseRelayClient : RelayInterface { @ExperimentalCoroutinesApi override fun subscribe(topic: String, id: Long?, onResult: (Result) -> Unit) { - val subscribeRequest = RelayDTO.Subscribe.Request(id = id ?: generateClientToServerId(), params = RelayDTO.Subscribe.Request.Params(Topic(topic))) - - if (isLoggingEnabled) { - logger.log("Sending SubscribeRequest: $subscribeRequest; timestamp: ${System.currentTimeMillis()}") - } - - observeSubscribeResult(subscribeRequest.id, onResult) - relayService.subscribeRequest(subscribeRequest) + connectAndCallRelay( + onConnected = { + val subscribeRequest = RelayDTO.Subscribe.Request(id = id ?: generateClientToServerId(), params = RelayDTO.Subscribe.Request.Params(Topic(topic))) + if (isLoggingEnabled) { + logger.log("Sending SubscribeRequest: $subscribeRequest; timestamp: ${System.currentTimeMillis()}") + } + observeSubscribeResult(subscribeRequest.id, onResult) + relayService.subscribeRequest(subscribeRequest) + }, + onFailure = { onResult(Result.failure(it)) } + ) } private fun observeSubscribeResult(id: Long, onResult: (Result) -> Unit) { @@ -172,18 +201,26 @@ abstract class BaseRelayClient : RelayInterface { @ExperimentalCoroutinesApi override fun batchSubscribe(topics: List, id: Long?, onResult: (Result) -> Unit) { - val batchSubscribeRequest = RelayDTO.BatchSubscribe.Request(id = id ?: generateClientToServerId(), params = RelayDTO.BatchSubscribe.Request.Params(topics)) - - observeBatchSubscribeResult(batchSubscribeRequest.id, onResult) - relayService.batchSubscribeRequest(batchSubscribeRequest) + connectAndCallRelay( + onConnected = { + if (!unAckedTopics.containsAll(topics)) { + unAckedTopics.addAll(topics) + val batchSubscribeRequest = RelayDTO.BatchSubscribe.Request(id = id ?: generateClientToServerId(), params = RelayDTO.BatchSubscribe.Request.Params(topics)) + observeBatchSubscribeResult(batchSubscribeRequest.id, topics, onResult) + relayService.batchSubscribeRequest(batchSubscribeRequest) + } + }, + onFailure = { onResult(Result.failure(it)) } + ) } - private fun observeBatchSubscribeResult(id: Long, onResult: (Result) -> Unit) { + private fun observeBatchSubscribeResult(id: Long, topics: List, onResult: (Result) -> Unit) { scope.launch { try { withTimeout(RESULT_TIMEOUT) { resultState .filterIsInstance() + .onEach { if (unAckedTopics.isNotEmpty()) unAckedTopics.removeAll(topics) } .filter { relayResult -> relayResult.id == id } .first { batchSubscribeResult -> when (batchSubscribeResult) { @@ -210,13 +247,18 @@ abstract class BaseRelayClient : RelayInterface { id: Long?, onResult: (Result) -> Unit, ) { - val unsubscribeRequest = RelayDTO.Unsubscribe.Request( - id = id ?: generateClientToServerId(), - params = RelayDTO.Unsubscribe.Request.Params(Topic(topic), SubscriptionId(subscriptionId)) - ) + connectAndCallRelay( + onConnected = { + val unsubscribeRequest = RelayDTO.Unsubscribe.Request( + id = id ?: generateClientToServerId(), + params = RelayDTO.Unsubscribe.Request.Params(Topic(topic), SubscriptionId(subscriptionId)) + ) - observeUnsubscribeResult(unsubscribeRequest.id, onResult) - relayService.unsubscribeRequest(unsubscribeRequest) + observeUnsubscribeResult(unsubscribeRequest.id, onResult) + relayService.unsubscribeRequest(unsubscribeRequest) + }, + onFailure = { onResult(Result.failure(it)) } + ) } private fun observeUnsubscribeResult(id: Long, onResult: (Result) -> Unit) { @@ -244,6 +286,98 @@ abstract class BaseRelayClient : RelayInterface { } } + private fun connectAndCallRelay(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { + when { + shouldConnect() -> { + isConnecting = true + connectionLifecycle.reconnect() + retryCount++ + + awaitConnectionWithRetry( + onConnected = { + isConnecting = false + retryCount = 0 + onConnected() + }, + onRetry = { error -> + if (retryCount == 3) { + isConnecting = false + retryCount = 0 + onFailure(error) + } else { + connectionLifecycle.reconnect() + retryCount++ + } + }, + onTimeout = { error -> onFailure(error) } + ) + } + + isConnecting -> awaitConnection(onConnected, onFailure) + connectionState.value == ConnectionState.Open -> onConnected() + } + } + + private fun awaitConnection(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { + scope.launch { + try { + withTimeout(CONNECTION_TIMEOUT) { + connectionState + .filter { state -> state != ConnectionState.Idle } + .collect { state -> + if (state == ConnectionState.Open) { + cancelJobIfActive() + onConnected() + } + } + } + } catch (e: TimeoutCancellationException) { + onFailure(e) + cancelJobIfActive() + } catch (e: Exception) { + if (e !is CancellationException) { + onFailure(e) + } + cancelJobIfActive() + } + } + } + + private fun awaitConnectionWithRetry(onConnected: () -> Unit, onRetry: (Throwable) -> Unit, onTimeout: (Throwable) -> Unit = {}) { + scope.launch { + try { + withTimeout(CONNECTION_TIMEOUT) { + connectionState + .filter { state -> state != ConnectionState.Idle } + .take(MAX_RETRIES) + .collect { state -> + if (state == ConnectionState.Open) { + cancelJobIfActive() + onConnected() + } else { + onRetry((state as ConnectionState.Closed).throwable) + } + } + } + } catch (e: TimeoutCancellationException) { + isConnecting = false + onTimeout(e) + cancelJobIfActive() + } catch (e: Exception) { + if (e !is CancellationException) { + onRetry(e) + } + } + } + } + + private fun shouldConnect() = !isConnecting && (connectionState.value is ConnectionState.Closed || connectionState.value is ConnectionState.Idle) + private fun getErrorMessage(event: WebSocket.Event) = when (event) { + is WebSocket.Event.OnConnectionClosed -> event.shutdownReason.reason + is WebSocket.Event.OnConnectionFailed -> event.throwable.message + else -> "Unknown" + } + private fun publishSubscriptionAcknowledgement(id: Long) { val publishRequest = RelayDTO.Subscription.Result.Acknowledgement(id = id, result = true) relayService.publishSubscriptionAcknowledgement(publishRequest) @@ -258,5 +392,7 @@ abstract class BaseRelayClient : RelayInterface { private companion object { const val REPLAY: Int = 1 const val RESULT_TIMEOUT: Long = 60000 + const val CONNECTION_TIMEOUT: Long = 15000 + const val MAX_RETRIES: Int = 3 } } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt b/foundation/src/main/kotlin/com/walletconnect/foundation/network/ConnectionLifecycle.kt similarity index 68% rename from core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt rename to foundation/src/main/kotlin/com/walletconnect/foundation/network/ConnectionLifecycle.kt index dcd3fc8d9..bdd927e93 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/connection/ConnectionLifecycle.kt +++ b/foundation/src/main/kotlin/com/walletconnect/foundation/network/ConnectionLifecycle.kt @@ -1,4 +1,4 @@ -package com.walletconnect.android.internal.common.connection +package com.walletconnect.foundation.network import kotlinx.coroutines.flow.StateFlow diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96fccff90..d002a0a2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ sqlDelight = "2.0.2" dokka = "1.9.20" moshi = "1.15.1" googleService = "4.4.1" -scarlet = "0.1.14-SNAPSHOT" #"1.0.1" +scarlet = "1.0.2" koin = "3.5.6" retrofit = "2.11.0" okhttp = "4.12.0" @@ -98,13 +98,13 @@ androidx-compose-material = { module = "androidx.compose.material:material", ver coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -scarlet = { module = "com.tinder.scarlet:scarlet", version.ref = "scarlet" } -scarlet-okhttp = { module = "com.tinder.scarlet:websocket-okhttp", version.ref = "scarlet" } -scarlet-coroutines = { module = "com.tinder.scarlet:stream-adapter-coroutines", version.ref = "scarlet" } -scarlet-moshi = { module = "com.tinder.scarlet:message-adapter-moshi", version.ref = "scarlet" } -scarlet-android = { module = "com.tinder.scarlet:lifecycle-android", version.ref = "scarlet" } -scarlet-mockwebserver = { module = "com.tinder.scarlet:websocket-mockwebserver", version.ref = "scarlet" } -scarlet-testUtils = { module = "com.tinder.scarlet:test-utils", version.ref = "scarlet" } +scarlet = { module = "com.walletconnect.Scarlet:scarlet", version.ref = "scarlet" } +scarlet-okhttp = { module = "com.walletconnect.Scarlet:websocket-okhttp", version.ref = "scarlet" } +scarlet-coroutines = { module = "com.walletconnect.Scarlet:stream-adapter-coroutines", version.ref = "scarlet" } +scarlet-moshi = { module = "com.walletconnect.Scarlet:message-adapter-moshi", version.ref = "scarlet" } +scarlet-android = { module = "com.walletconnect.Scarlet:lifecycle-android", version.ref = "scarlet" } +scarlet-mockwebserver = { module = "com.walletconnect.Scarlet:websocket-mockwebserver", version.ref = "scarlet" } +scarlet-testUtils = { module = "com.walletconnect.Scarlet:test-utils", version.ref = "scarlet" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } diff --git a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt index af952d5a4..0a0405891 100644 --- a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt +++ b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/NotifyEngine.kt @@ -104,9 +104,8 @@ internal class NotifyEngine( .onEach { supervisorScope { launch(Dispatchers.IO) { -// println("kobe: Notify batch subs") -// resubscribeToSubscriptions() -// watchSubscriptionsForEveryRegisteredAccount() + resubscribeToSubscriptions() + watchSubscriptionsForEveryRegisteredAccount() } } diff --git a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt index 60f982e4a..da68fcd12 100644 --- a/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt +++ b/protocol/notify/src/main/kotlin/com/walletconnect/notify/engine/domain/WatchSubscriptionsUseCase.kt @@ -4,7 +4,9 @@ package com.walletconnect.notify.engine.domain import com.walletconnect.android.internal.common.crypto.kmr.KeyManagementRepository import com.walletconnect.android.internal.common.model.AccountId +import com.walletconnect.android.internal.common.model.EnvelopeType import com.walletconnect.android.internal.common.model.IrnParams +import com.walletconnect.android.internal.common.model.Participants import com.walletconnect.android.internal.common.model.Tags import com.walletconnect.android.internal.common.model.params.CoreNotifyParams import com.walletconnect.android.internal.common.model.type.RelayJsonRpcInteractorInterface @@ -33,8 +35,7 @@ internal class WatchSubscriptionsUseCase( val selfPublicKey = getSelfKeyForWatchSubscriptionUseCase(requestTopic, accountId) val responseTopic = keyManagementRepository.generateTopicFromKeyAgreement(selfPublicKey, peerPublicKey) -// println("kobe: watch") -// jsonRpcInteractor.subscribe(responseTopic) { error -> onFailure(error) } + jsonRpcInteractor.subscribe(responseTopic) { error -> onFailure(error) } val account = registeredAccountsRepository.getAccountByAccountId(accountId.value) val didJwt = fetchDidJwtInteractor.watchSubscriptionsRequest(accountId, authenticationPublicKey, account.appDomain) @@ -45,14 +46,14 @@ internal class WatchSubscriptionsUseCase( val request = NotifyRpc.NotifyWatchSubscriptions(params = watchSubscriptionsParams) val irnParams = IrnParams(Tags.NOTIFY_WATCH_SUBSCRIPTIONS, Ttl(thirtySeconds)) -// jsonRpcInteractor.publishJsonRpcRequest( -// topic = requestTopic, -// params = irnParams, -// payload = request, -// envelopeType = EnvelopeType.ONE, -// participants = Participants(selfPublicKey, peerPublicKey), -// onSuccess = onSuccess, -// onFailure = onFailure -// ) + jsonRpcInteractor.publishJsonRpcRequest( + topic = requestTopic, + params = irnParams, + payload = request, + envelopeType = EnvelopeType.ONE, + participants = Participants(selfPublicKey, peerPublicKey), + onSuccess = onSuccess, + onFailure = onFailure + ) } } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt index eee6271ca..6fc5ce208 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt @@ -580,11 +580,11 @@ class SignProtocol(private val koinApp: KoinApplication = wcKoinApp) : SignInter atomicBoolean?.set(true) onDelegate(Sign.Model.ConnectionState(true)) } - - atomicBoolean?.get() == false && connectionState is WSSConnectionState.Disconnected.ConnectionFailed -> { - atomicBoolean?.set(false) - onDelegate(Sign.Model.ConnectionState(false, Sign.Model.ConnectionState.Reason.ConnectionFailed(connectionState.throwable))) - } +// +// atomicBoolean?.get() == false && connectionState is WSSConnectionState.Disconnected.ConnectionFailed -> { +// atomicBoolean?.set(false) +// onDelegate(Sign.Model.ConnectionState(false, Sign.Model.ConnectionState.Reason.ConnectionFailed(connectionState.throwable))) +// } else -> Unit } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt index 834168dd9..bd9366ded 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/domain/SignEngine.kt @@ -330,7 +330,6 @@ internal class SignEngine( } val validSessionTopics = listOfValidSessions.map { it.topic.value } - println("kobe: re-subscribe to session topics: $validSessionTopics") jsonRpcInteractor.batchSubscribe(validSessionTopics) { error -> scope.launch { _engineEvent.emit(SDKError(error)) } } } catch (e: Exception) { scope.launch { _engineEvent.emit(SDKError(e)) } @@ -341,7 +340,6 @@ internal class SignEngine( scope.launch { try { val responseTopics = authenticateResponseTopicRepository.getResponseTopics().map { responseTopic -> responseTopic } - println("kobe: re-subscribe to pending auth topics; $responseTopics") jsonRpcInteractor.batchSubscribe(responseTopics) { error -> scope.launch { _engineEvent.emit(SDKError(error)) } } } catch (e: Exception) { scope.launch { _engineEvent.emit(SDKError(e)) } diff --git a/sample/dapp/src/main/kotlin/com/walletconnect/sample/dapp/ui/routes/host/DappSampleHost.kt b/sample/dapp/src/main/kotlin/com/walletconnect/sample/dapp/ui/routes/host/DappSampleHost.kt index 2b1074a2b..48eb12e5f 100644 --- a/sample/dapp/src/main/kotlin/com/walletconnect/sample/dapp/ui/routes/host/DappSampleHost.kt +++ b/sample/dapp/src/main/kotlin/com/walletconnect/sample/dapp/ui/routes/host/DappSampleHost.kt @@ -87,20 +87,29 @@ fun DappSampleHost() { @Composable private fun NoConnectionIndicator() { - Row( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF3496ff)) - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text(text = "No internet connection", color = Color.White) - Spacer(modifier = Modifier.width(4.dp)) - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_offline), - contentDescription = null, - modifier = Modifier.size(24.dp), - colorFilter = ColorFilter.tint(color = Color.White) - ) + var shouldShow by remember { mutableStateOf(true) } + + LaunchedEffect(key1 = Unit) { + delay(3000) + shouldShow = false + } + + if (shouldShow) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF3496ff)) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Text(text = "No internet connection", color = Color.White) + Spacer(modifier = Modifier.width(4.dp)) + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_offline), + contentDescription = null, + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(color = Color.White) + ) + } } } @@ -109,7 +118,7 @@ private fun RestoredConnectionIndicator() { var shouldShow by remember { mutableStateOf(true) } LaunchedEffect(key1 = Unit) { - delay(2000) + delay(3000) shouldShow = false } if (shouldShow) { diff --git a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt index 0672c98d7..97e343ea3 100644 --- a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt +++ b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/Web3WalletApplication.kt @@ -74,7 +74,7 @@ class Web3WalletApplication : Application() { metaData = appMetaData, onError = { error -> Firebase.crashlytics.recordException(error.throwable) - println("kobe: core init: ${error.throwable.stackTraceToString()}") + println("Init error: ${error.throwable.stackTraceToString()}") scope.launch { connectionStateFlow.emit(ConnectionState.Error(error.throwable.message ?: "")) } From d50bfc56afa3c44c068da1b218d8c5a33de8cf34 Mon Sep 17 00:00:00 2001 From: kubel Date: Mon, 2 Sep 2024 12:43:08 +0200 Subject: [PATCH 5/7] Handle connection retries --- .../domain/relay/RelayJsonRpcInteractor.kt | 4 +- .../android/relay/NetworkClientTimeout.kt | 2 +- .../android/relay/RelayClient.kt | 3 - .../foundation/network/BaseRelayClient.kt | 110 +++++++++--------- .../wallet/ui/routes/host/WalletSampleHost.kt | 12 +- 5 files changed, 66 insertions(+), 65 deletions(-) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt index 52f0e8e27..f967bc602 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt @@ -76,7 +76,7 @@ internal class RelayJsonRpcInteractor( override val internalErrors: SharedFlow = _internalErrors.asSharedFlow() override val wssConnectionState: StateFlow get() = relay.wssConnectionState - private var subscriptions = ObservableMap(mutableMapOf()) { newMap -> backoffStrategy.shouldBackoff(newMap.isNotEmpty()) } + private var subscriptions = ObservableMap { newMap -> if (newMap.isEmpty()) backoffStrategy.shouldBackoff(false) } override val onResubscribe: Flow = relay.onResubscribe init { @@ -171,6 +171,7 @@ internal class RelayJsonRpcInteractor( } try { + backoffStrategy.shouldBackoff(true) relay.subscribe(topic.value) { result -> result.fold( onSuccess = { acknowledgement -> @@ -197,6 +198,7 @@ internal class RelayJsonRpcInteractor( } if (topics.isNotEmpty()) { + backoffStrategy.shouldBackoff(true) try { relay.batchSubscribe(topics) { result -> result.fold( diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/NetworkClientTimeout.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/NetworkClientTimeout.kt index a500ff355..7ee752147 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/NetworkClientTimeout.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/NetworkClientTimeout.kt @@ -20,7 +20,7 @@ data class NetworkClientTimeout( companion object { - private const val MIN_TIMEOUT_LIMIT_AS_MILLIS = 10_000L + private const val MIN_TIMEOUT_LIMIT_AS_MILLIS = 15_000L private const val MAX_TIMEOUT_LIMIT_AS_MILLIS = 60_000L fun getDefaultTimeout() = NetworkClientTimeout( diff --git a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt index 9b8909ff1..e9d556325 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/relay/RelayClient.kt @@ -82,9 +82,6 @@ class RelayClient(private val koinApp: KoinApplication = wcKoinApp) : BaseRelayC event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Connected -> _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) -// event is Relay.Model.Event.OnConnectionFailed && _wssConnectionState.value is WSSConnectionState.Disconnected.ConnectionClosed -> -// _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionFailed(event.throwable.toWalletConnectException) - event is Relay.Model.Event.OnConnectionClosed && _wssConnectionState.value is WSSConnectionState.Connected -> _wssConnectionState.value = WSSConnectionState.Disconnected.ConnectionClosed("Connection closed: ${event.shutdownReason.reason} ${event.shutdownReason.code}") } diff --git a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt index 6408069fd..70ae5d3e0 100644 --- a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt +++ b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -92,13 +93,11 @@ abstract class BaseRelayClient : RelayInterface { if (event is WebSocket.Event.OnConnectionOpened<*>) { connectionState.value = ConnectionState.Open } else if (event is WebSocket.Event.OnConnectionClosed || event is WebSocket.Event.OnConnectionFailed) { - connectionState.value = ConnectionState.Closed(Throwable(getErrorMessage(event))) + connectionState.value = ConnectionState.Closed(getError(event)) } } .map { event -> - if (isLoggingEnabled) { - println("Event: $event") - } + logger.log("Event: $event") event.toRelayEvent() } .shareIn(scope, SharingStarted.Lazily, REPLAY) @@ -288,47 +287,40 @@ abstract class BaseRelayClient : RelayInterface { private fun connectAndCallRelay(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { when { - shouldConnect() -> { - isConnecting = true - connectionLifecycle.reconnect() - retryCount++ - - awaitConnectionWithRetry( - onConnected = { - isConnecting = false - retryCount = 0 - onConnected() - }, - onRetry = { error -> - if (retryCount == 3) { - isConnecting = false - retryCount = 0 - onFailure(error) - } else { - connectionLifecycle.reconnect() - retryCount++ - } - }, - onTimeout = { error -> onFailure(error) } - ) - } - + shouldConnect() -> connect(onConnected, onFailure) isConnecting -> awaitConnection(onConnected, onFailure) connectionState.value == ConnectionState.Open -> onConnected() } } - private fun awaitConnection(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { + private fun shouldConnect() = !isConnecting && (connectionState.value is ConnectionState.Closed || connectionState.value is ConnectionState.Idle) + private fun connect(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { + isConnecting = true + connectionLifecycle.reconnect() + awaitConnectionWithRetry( + onConnected = { + reset() + onConnected() + }, + onFailure = { error -> + reset() + onFailure(error) + } + ) + } + + private fun awaitConnectionWithRetry(onConnected: () -> Unit, onFailure: (Throwable) -> Unit = {}) { scope.launch { try { withTimeout(CONNECTION_TIMEOUT) { connectionState .filter { state -> state != ConnectionState.Idle } - .collect { state -> - if (state == ConnectionState.Open) { - cancelJobIfActive() - onConnected() - } + .take(4) + .onEach { state -> handleRetries(state, onFailure) } + .filter { state -> state == ConnectionState.Open } + .firstOrNull { + onConnected() + true } } } catch (e: TimeoutCancellationException) { @@ -338,44 +330,49 @@ abstract class BaseRelayClient : RelayInterface { if (e !is CancellationException) { onFailure(e) } - cancelJobIfActive() } } } - private fun awaitConnectionWithRetry(onConnected: () -> Unit, onRetry: (Throwable) -> Unit, onTimeout: (Throwable) -> Unit = {}) { + private fun awaitConnection(onConnected: () -> Unit, onFailure: (Throwable) -> Unit) { scope.launch { try { withTimeout(CONNECTION_TIMEOUT) { connectionState - .filter { state -> state != ConnectionState.Idle } - .take(MAX_RETRIES) - .collect { state -> - if (state == ConnectionState.Open) { - cancelJobIfActive() - onConnected() - } else { - onRetry((state as ConnectionState.Closed).throwable) - } + .filter { state -> state is ConnectionState.Open } + .firstOrNull { + onConnected() + true } } } catch (e: TimeoutCancellationException) { - isConnecting = false - onTimeout(e) + onFailure(e) cancelJobIfActive() } catch (e: Exception) { if (e !is CancellationException) { - onRetry(e) + onFailure(e) } + cancelJobIfActive() } } } - private fun shouldConnect() = !isConnecting && (connectionState.value is ConnectionState.Closed || connectionState.value is ConnectionState.Idle) - private fun getErrorMessage(event: WebSocket.Event) = when (event) { - is WebSocket.Event.OnConnectionClosed -> event.shutdownReason.reason - is WebSocket.Event.OnConnectionFailed -> event.throwable.message - else -> "Unknown" + private fun CoroutineScope.handleRetries(state: ConnectionState, onFailure: (Throwable) -> Unit) { + if (state is ConnectionState.Closed) { + if (retryCount == MAX_RETRIES) { + onFailure(Throwable("Connectivity error, please check your Internet connection and try again")) + cancelJobIfActive() + } else { + connectionLifecycle.reconnect() + retryCount++ + } + } + } + + private fun getError(event: WebSocket.Event): Throwable = when (event) { + is WebSocket.Event.OnConnectionClosed -> Throwable(event.shutdownReason.reason) + is WebSocket.Event.OnConnectionFailed -> event.throwable + else -> Throwable("Unknown") } private fun publishSubscriptionAcknowledgement(id: Long) { @@ -389,6 +386,11 @@ abstract class BaseRelayClient : RelayInterface { } } + private fun reset() { + isConnecting = false + retryCount = 0 + } + private companion object { const val REPLAY: Int = 1 const val RESULT_TIMEOUT: Long = 60000 diff --git a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt index d564d898d..3962ee6e1 100644 --- a/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt +++ b/sample/wallet/src/main/kotlin/com/walletconnect/sample/wallet/ui/routes/host/WalletSampleHost.kt @@ -111,14 +111,14 @@ fun WalletSampleHost( ErrorBanner((connectionState as ConnectionState.Error).message) } else if (connectionState is ConnectionState.Ok) { RestoredConnectionBanner() + } - if (isLoader) { - Loader(initMessage = "WalletConnect is pairing...", updateMessage = "Pairing is taking longer than usual, please try again...") - } + if (isLoader) { + Loader(initMessage = "WalletConnect is pairing...", updateMessage = "Pairing is taking longer than usual, please try again...") + } - if (isRequestLoader) { - Loader(initMessage = "Awaiting a request...", updateMessage = "It is taking longer than usual..") - } + if (isRequestLoader) { + Loader(initMessage = "Awaiting a request...", updateMessage = "It is taking longer than usual..") } Timer(web3walletViewModel) From 3eb4a7c67af8461cc3e9a61c4aecda4004d4e07e Mon Sep 17 00:00:00 2001 From: kubel Date: Mon, 2 Sep 2024 12:52:38 +0200 Subject: [PATCH 6/7] Clean up --- .../android/internal/common/di/CoreJsonRpcModule.kt | 7 +------ .../kotlin/com/walletconnect/sign/client/SignProtocol.kt | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt index f08944b30..688922b84 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreJsonRpcModule.kt @@ -1,8 +1,6 @@ package com.walletconnect.android.internal.common.di import com.squareup.moshi.Moshi -import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle -import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer import com.walletconnect.android.internal.common.json_rpc.domain.link_mode.LinkModeJsonRpcInteractor import com.walletconnect.android.internal.common.json_rpc.domain.link_mode.LinkModeJsonRpcInteractorInterface @@ -11,20 +9,17 @@ import com.walletconnect.android.internal.common.model.type.RelayJsonRpcInteract import com.walletconnect.android.internal.common.model.type.SerializableJsonRpc import com.walletconnect.android.pairing.model.PairingJsonRpcMethod import com.walletconnect.android.pairing.model.PairingRpc -import com.walletconnect.android.relay.ConnectionType -import com.walletconnect.foundation.network.ConnectionLifecycle import com.walletconnect.utils.JsonAdapterEntry import com.walletconnect.utils.addDeserializerEntry import com.walletconnect.utils.addSerializerEntry import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named -import org.koin.core.scope.Scope import org.koin.dsl.module import kotlin.reflect.KClass @JvmSynthetic -fun coreJsonRpcModule(connectionType: ConnectionType) = module { +fun coreJsonRpcModule() = module { single { RelayJsonRpcInteractor( diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt index 6fc5ce208..ca5333dcb 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/client/SignProtocol.kt @@ -580,11 +580,6 @@ class SignProtocol(private val koinApp: KoinApplication = wcKoinApp) : SignInter atomicBoolean?.set(true) onDelegate(Sign.Model.ConnectionState(true)) } -// -// atomicBoolean?.get() == false && connectionState is WSSConnectionState.Disconnected.ConnectionFailed -> { -// atomicBoolean?.set(false) -// onDelegate(Sign.Model.ConnectionState(false, Sign.Model.ConnectionState.Reason.ConnectionFailed(connectionState.throwable))) -// } else -> Unit } From 598e3f304bce6e7721b71614cb2e275cb72d4787 Mon Sep 17 00:00:00 2001 From: kubel Date: Tue, 3 Sep 2024 07:13:39 +0200 Subject: [PATCH 7/7] Test fixing --- .../com/walletconnect/android/CoreProtocol.kt | 2 +- .../domain/relay/RelayJsonRpcInteractor.kt | 2 +- .../android/internal/EventsRepositoryTest.kt | 14 +- .../android/internal/RelayClientTests.kt | 8 +- .../internal/domain/RelayerInteractorTest.kt | 6 +- .../foundation/network/BaseRelayClient.kt | 2 +- .../foundation/BaseRelayClientTest.kt | 463 ++++++++++-------- .../ApproveSessionAuthenticateUseCaseTest.kt | 4 +- 8 files changed, 275 insertions(+), 226 deletions(-) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt b/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt index cf5fddf4f..d90923d32 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt @@ -176,7 +176,7 @@ class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInter module { single { Echo } }, module { single { Push } }, module { single { Verify } }, - coreJsonRpcModule(connectionType), + coreJsonRpcModule(), corePairingModule(Pairing, PairingController), keyServerModule(keyServerUrl), explorerModule(), diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt index f967bc602..eb0da508c 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/json_rpc/domain/relay/RelayJsonRpcInteractor.kt @@ -77,7 +77,7 @@ internal class RelayJsonRpcInteractor( override val wssConnectionState: StateFlow get() = relay.wssConnectionState private var subscriptions = ObservableMap { newMap -> if (newMap.isEmpty()) backoffStrategy.shouldBackoff(false) } - override val onResubscribe: Flow = relay.onResubscribe + override val onResubscribe: Flow get() = relay.onResubscribe init { manageSubscriptions() diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/EventsRepositoryTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/EventsRepositoryTest.kt index e84b3f985..e509ac1bd 100644 --- a/core/android/src/test/kotlin/com/walletconnect/android/internal/EventsRepositoryTest.kt +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/EventsRepositoryTest.kt @@ -46,7 +46,7 @@ class EventsRepositoryTest { @Test fun `insertOrAbort should insert event when telemetry is enabled`() = runTest(testDispatcher) { val props = Props(event = "testEvent", type = "testType") - every { eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just Runs + every { eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just Runs repository.insertOrAbortTelemetry(props) @@ -61,7 +61,8 @@ class EventsRepositoryTest { trace = null, correlation_id = any(), client_id = any(), - direction = any() + direction = any(), + user_agent = any() ) } } @@ -69,7 +70,7 @@ class EventsRepositoryTest { @Test fun `insertOrAbort should insert event `() = runTest(testDispatcher) { val props = Props(event = "testEvent", type = "testType") - every { eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just Runs + every { eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just Runs repository.insertOrAbort(props) @@ -84,7 +85,8 @@ class EventsRepositoryTest { trace = null, correlation_id = any(), client_id = any(), - direction = any() + direction = any(), + user_agent = any() ) } } @@ -97,14 +99,14 @@ class EventsRepositoryTest { repository.insertOrAbortTelemetry(props) verify(exactly = 0) { - eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) + eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } } @Test fun `insertOrAbort should throw SQLiteException when insertion fails`() = runTest(testDispatcher) { val props = Props(event = "testEvent", type = "testType") - every { eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } throws SQLiteException() + every { eventQueries.insertOrAbort(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } throws SQLiteException() assertFailsWith { repository.insertOrAbortTelemetry(props) diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/RelayClientTests.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/RelayClientTests.kt index c978a77fc..a594ec549 100644 --- a/core/android/src/test/kotlin/com/walletconnect/android/internal/RelayClientTests.kt +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/RelayClientTests.kt @@ -2,6 +2,8 @@ package com.walletconnect.android.internal import com.tinder.scarlet.WebSocket import com.walletconnect.android.internal.common.connection.ConnectivityState +import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle +import com.walletconnect.android.internal.common.connection.ManualConnectionLifecycle import com.walletconnect.android.internal.common.di.AndroidCommonDITags import com.walletconnect.android.internal.common.scope import com.walletconnect.android.relay.ConnectionType @@ -34,6 +36,8 @@ import org.koin.dsl.module class RelayClientTests { private lateinit var relayClient: RelayClient private val mockRelayService = mockk(relaxed = true) + private val defaultConnectionLifecycleMock = mockk(relaxed = true) + private val manualConnectionLifecycleMock = mockk(relaxed = true) private val mockLogger = mockk(relaxed = true) private val mockNetworkState = mockk(relaxed = true) private val testDispatcher = StandardTestDispatcher() @@ -47,6 +51,8 @@ class RelayClientTests { single(named(AndroidCommonDITags.RELAY_SERVICE)) { mockRelayService } single(named(AndroidCommonDITags.LOGGER)) { mockLogger } single(named(AndroidCommonDITags.CONNECTIVITY_STATE)) { mockNetworkState } + single(named(AndroidCommonDITags.MANUAL_CONNECTION_LIFECYCLE)) { manualConnectionLifecycleMock } + single(named(AndroidCommonDITags.DEFAULT_CONNECTION_LIFECYCLE)) { defaultConnectionLifecycleMock } }) } @@ -73,7 +79,7 @@ class RelayClientTests { relayClient.initialize(ConnectionType.MANUAL) { error -> assertEquals( - "Error while connecting, please check your Internet connection or contact support: java.lang.Throwable: Network failure", + "Error while connecting, please check your Internet connection or contact support: Network failure", error.message ) scope.coroutineContext.cancelChildren() diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt index ee273e612..ea4a6d2db 100644 --- a/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/domain/RelayerInteractorTest.kt @@ -2,7 +2,6 @@ package com.walletconnect.android.internal.domain import com.walletconnect.android.internal.common.ConditionalExponentialBackoffStrategy import com.walletconnect.android.internal.common.JsonRpcResponse -import com.walletconnect.android.internal.common.connection.DefaultConnectionLifecycle import com.walletconnect.android.internal.common.crypto.codec.Codec import com.walletconnect.android.internal.common.exception.WalletConnectException import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer @@ -42,8 +41,6 @@ internal class RelayerInteractorTest { every { subscriptionRequest } returns flow { } } - private val defaultConnectionLifecycle: DefaultConnectionLifecycle = mockk() - private val backoffStrategy: ConditionalExponentialBackoffStrategy = mockk() private val jsonRpcHistory: JsonRpcHistory = mockk { @@ -76,8 +73,9 @@ internal class RelayerInteractorTest { } private val sut = - spyk(RelayJsonRpcInteractor(relay, codec, jsonRpcHistory, pushMessagesRepository, logger, defaultConnectionLifecycle, backoffStrategy), recordPrivateCalls = true) { + spyk(RelayJsonRpcInteractor(relay, codec, jsonRpcHistory, pushMessagesRepository, logger, backoffStrategy), recordPrivateCalls = true) { every { checkNetworkConnectivity() } answers { } + every { relay.onResubscribe } returns flow { } } private val topicVO = Topic("mockkTopic") diff --git a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt index 70ae5d3e0..6d73522e4 100644 --- a/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt +++ b/foundation/src/main/kotlin/com/walletconnect/foundation/network/BaseRelayClient.kt @@ -52,7 +52,7 @@ abstract class BaseRelayClient : RelayInterface { lateinit var connectionLifecycle: ConnectionLifecycle protected var logger: Logger private val resultState: MutableSharedFlow = MutableSharedFlow() - private var connectionState: MutableStateFlow = MutableStateFlow(ConnectionState.Idle) + internal var connectionState: MutableStateFlow = MutableStateFlow(ConnectionState.Idle) private var unAckedTopics: MutableList = mutableListOf() private var isConnecting: Boolean = false private var retryCount: Int = 0 diff --git a/foundation/src/test/kotlin/com/walletconnect/foundation/BaseRelayClientTest.kt b/foundation/src/test/kotlin/com/walletconnect/foundation/BaseRelayClientTest.kt index 9bc21c2f1..24e577221 100644 --- a/foundation/src/test/kotlin/com/walletconnect/foundation/BaseRelayClientTest.kt +++ b/foundation/src/test/kotlin/com/walletconnect/foundation/BaseRelayClientTest.kt @@ -1,19 +1,28 @@ package com.walletconnect.foundation +import com.tinder.scarlet.WebSocket import com.walletconnect.foundation.common.model.SubscriptionId import com.walletconnect.foundation.network.BaseRelayClient +import com.walletconnect.foundation.network.ConnectionLifecycle +import com.walletconnect.foundation.network.ConnectionState import com.walletconnect.foundation.network.data.service.RelayService import com.walletconnect.foundation.network.model.Relay import com.walletconnect.foundation.network.model.RelayDTO import com.walletconnect.foundation.util.Logger import com.walletconnect.foundation.util.scope +import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher @@ -32,214 +41,248 @@ import org.junit.Test @ExperimentalCoroutinesApi class BaseRelayClientTest { - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - private lateinit var client: BaseRelayClient - private val relayServiceMock = mockk(relaxed = true) - private val loggerMock = mockk(relaxed = true) - - @Before - fun setup() { - Dispatchers.setMain(testDispatcher) - - client = object : BaseRelayClient() { - init { - this.relayService = relayServiceMock - this.logger = loggerMock - scope = testScope - } - } - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `test publish success`() = testScope.runTest { - val topic = "testTopic" - val message = "testMessage" - val params = Relay.Model.IrnParams(1, 60, true) - val ack = RelayDTO.Publish.Result.Acknowledgement(123L, result = true) - - coEvery { relayServiceMock.publishRequest(any()) } returns Unit - coEvery { relayServiceMock.observePublishAcknowledgement() } returns flowOf(ack) - - client.observeResults() - client.publish(topic, message, params, 123L) { result -> - result.fold( - onSuccess = { - assertEquals(123L, it.id) - }, - onFailure = { fail(it.message) } - ) - } - - coVerify { relayServiceMock.publishRequest(any()) } - } - - @Test - fun `test publish error due to time out`() = testScope.runTest { - val topic = "testTopic" - val message = "testMessage" - val params = Relay.Model.IrnParams(1, 60, true) - - coEvery { relayServiceMock.publishRequest(any()) } returns Unit - coEvery { relayServiceMock.observePublishAcknowledgement() } returns flow { delay(10000L) } - - withContext(Dispatchers.Default.limitedParallelism(1)) { - client.publish(topic, message, params) { result -> - result.fold( - onSuccess = { - fail("Should not be successful") - }, - onFailure = { - assertTrue(result.exceptionOrNull() is TimeoutCancellationException) - } - ) - } - } - - advanceUntilIdle() - - coVerify { relayServiceMock.publishRequest(any()) } - } - - @Test - fun `test subscribe success`() = testScope.runTest { - val topic = "testTopic" - val expectedId = 123L - val relayDto = RelayDTO.Subscribe.Result.Acknowledgement(id = expectedId, result = SubscriptionId("testId")) - - coEvery { relayServiceMock.subscribeRequest(any()) } returns Unit - coEvery { relayServiceMock.observeSubscribeAcknowledgement() } returns flowOf(relayDto) - - client.observeResults() - client.subscribe(topic, expectedId) { result -> - result.fold( - onSuccess = { - assertEquals(expectedId, result.getOrNull()?.id) - }, - onFailure = { fail(it.message) } - ) - } - - coVerify { relayServiceMock.subscribeRequest(any()) } - } - - @Test - fun `test subscribe failure due to timeout`() = testScope.runTest() { - val topic = "testTopic" - - coEvery { relayServiceMock.subscribeRequest(any()) } returns Unit - coEvery { relayServiceMock.observeSubscribeAcknowledgement() } returns flow { delay(10000L) } - - client.subscribe(topic) { result -> - result.fold( - onSuccess = { - fail("Should not be successful") - }, - onFailure = { - assertTrue(result.exceptionOrNull() is TimeoutCancellationException) - } - ) - } - - testScheduler.apply { advanceTimeBy(5000); runCurrent() } - - coVerify { relayServiceMock.subscribeRequest(any()) } - } - - @Test - fun `test batch subscribe success`() = testScope.runTest { - val topics = listOf("testTopic") - val expectedId = 123L - val relayDto = RelayDTO.BatchSubscribe.Result.Acknowledgement(id = expectedId, result = listOf("testId")) - - coEvery { relayServiceMock.batchSubscribeRequest(any()) } returns Unit - coEvery { relayServiceMock.observeBatchSubscribeAcknowledgement() } returns flowOf(relayDto) - - client.observeResults() - client.batchSubscribe(topics, expectedId) { result -> - result.fold( - onSuccess = { - assertEquals(expectedId, result.getOrNull()?.id) - }, - onFailure = { fail(it.message) } - ) - } - - coVerify { relayServiceMock.batchSubscribeRequest(any()) } - } - - @Test - fun `test batch subscribe failure due to timeout`() = testScope.runTest { - val topics = listOf("testTopic") - - coEvery { relayServiceMock.batchSubscribeRequest(any()) } returns Unit - coEvery { relayServiceMock.observeBatchSubscribeAcknowledgement() } returns flow { delay(10000L) } - - client.batchSubscribe(topics) { result -> - result.fold( - onSuccess = { - fail("Should not be successful") - }, - onFailure = { - assertTrue(result.exceptionOrNull() is TimeoutCancellationException) - } - ) - - } - - testScheduler.apply { advanceTimeBy(5000); runCurrent() } - - coVerify { relayServiceMock.batchSubscribeRequest(any()) } - } - - @Test - fun `test unsubscribe success`() = testScope.runTest { - val topic = "testTopic" - val expectedId = 123L - val relayDto = RelayDTO.Unsubscribe.Result.Acknowledgement(id = expectedId, result = true) - - coEvery { relayServiceMock.unsubscribeRequest(any()) } returns Unit - coEvery { relayServiceMock.observeUnsubscribeAcknowledgement() } returns flowOf(relayDto) - - client.observeResults() - client.unsubscribe(topic, "subsId", expectedId) { result -> - result.fold( - onSuccess = { - assertEquals(expectedId, result.getOrNull()?.id) - }, - onFailure = { fail(it.message) } - ) - } - - coVerify { relayServiceMock.unsubscribeRequest(any()) } - } - - @Test - fun `test unsubscribe failure`() = testScope.runTest { - val topic = "testTopic" - - coEvery { relayServiceMock.subscribeRequest(any()) } returns Unit - coEvery { relayServiceMock.observeSubscribeAcknowledgement() } returns flow { delay(10000L) } - - client.subscribe(topic) { result -> - result.fold( - onSuccess = { - fail("Should not be successful") - }, - onFailure = { - assertTrue(result.exceptionOrNull() is TimeoutCancellationException) - } - ) - } - - testScheduler.apply { advanceTimeBy(5000); runCurrent() } - - coVerify { relayServiceMock.subscribeRequest(any()) } - } - + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private lateinit var client: BaseRelayClient + private val relayServiceMock = mockk(relaxed = true) + private val connectionLifecycleMock = mockk(relaxed = true) + private val loggerMock = mockk(relaxed = true) + private val mockConnectionState = MutableStateFlow(ConnectionState.Idle) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + + client = spyk(object : BaseRelayClient() { + init { + this.relayService = relayServiceMock + this.logger = loggerMock + scope = testScope + this.connectionLifecycle = connectionLifecycleMock + } + }, recordPrivateCalls = true) + client.connectionState = mockConnectionState + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `publish invokes relayService publishRequest successfully`() = testScope.runTest { + val id = 123L + + val topic = "testTopic" + val message = "testMessage" + val params = Relay.Model.IrnParams(1, 60, true) + val ack = RelayDTO.Publish.Result.Acknowledgement(123L, result = true) + + val publishRequestSlot = slot() + coEvery { relayServiceMock.publishRequest(capture(publishRequestSlot)) } just Runs + coEvery { relayServiceMock.observePublishAcknowledgement() } returns flowOf(ack) + every { connectionLifecycleMock.reconnect() } just Runs + coEvery { relayServiceMock.observeWebSocketEvent() } returns flowOf(WebSocket.Event.OnConnectionOpened("Open")) + + client.connectionState.value = ConnectionState.Open + client.publish(topic, message, params, id) + + coVerify { relayServiceMock.publishRequest(any()) } + assertEquals(id, publishRequestSlot.captured.id) + } + + @Test + fun `test publish success`() = testScope.runTest { + val topic = "testTopic" + val message = "testMessage" + val params = Relay.Model.IrnParams(1, 60, true) + val ack = RelayDTO.Publish.Result.Acknowledgement(123L, result = true) + + coEvery { relayServiceMock.publishRequest(any()) } returns Unit + coEvery { relayServiceMock.observePublishAcknowledgement() } returns flowOf(ack) + coEvery { relayServiceMock.observeWebSocketEvent() } returns flowOf(WebSocket.Event.OnConnectionOpened("Open")) + + client.observeResults() + client.connectionState.value = ConnectionState.Open + client.publish(topic, message, params, 123L) { result -> + result.fold( + onSuccess = { + assertEquals(123L, it.id) + }, + onFailure = { fail(it.message) } + ) + } + + coVerify { relayServiceMock.publishRequest(any()) } + } + + + @Test + fun `test publish error due to time out`() = testScope.runTest() { + val topic = "testTopic" + val message = "testMessage" + val params = Relay.Model.IrnParams(1, 60, true) + + coEvery { relayServiceMock.publishRequest(any()) } returns Unit + coEvery { relayServiceMock.observePublishAcknowledgement() } returns flow { delay(15000L) } + + withContext(Dispatchers.Default.limitedParallelism(1)) { + client.connectionState.value = ConnectionState.Open + client.publish(topic, message, params) { result -> + result.fold( + onSuccess = { + fail("Should not be successful") + }, + onFailure = { + assertTrue(result.exceptionOrNull() is TimeoutCancellationException) + } + ) + } + } + + advanceUntilIdle() + + coVerify { relayServiceMock.publishRequest(any()) } + } + + @Test + fun `test subscribe success`() = testScope.runTest { + val topic = "testTopic" + val expectedId = 123L + val relayDto = RelayDTO.Subscribe.Result.Acknowledgement(id = expectedId, result = SubscriptionId("testId")) + + coEvery { relayServiceMock.subscribeRequest(any()) } returns Unit + coEvery { relayServiceMock.observeSubscribeAcknowledgement() } returns flowOf(relayDto) + + client.observeResults() + client.connectionState.value = ConnectionState.Open + client.subscribe(topic, expectedId) { result -> + result.fold( + onSuccess = { + assertEquals(expectedId, result.getOrNull()?.id) + }, + onFailure = { fail(it.message) } + ) + } + + coVerify { relayServiceMock.subscribeRequest(any()) } + } + + @Test + fun `test subscribe failure due to timeout`() = testScope.runTest() { + val topic = "testTopic" + + coEvery { relayServiceMock.subscribeRequest(any()) } returns Unit + coEvery { relayServiceMock.observeSubscribeAcknowledgement() } returns flow { delay(10000L) } + + client.connectionState.value = ConnectionState.Open + client.subscribe(topic) { result -> + result.fold( + onSuccess = { + fail("Should not be successful") + }, + onFailure = { + assertTrue(result.exceptionOrNull() is TimeoutCancellationException) + } + ) + } + + testScheduler.apply { advanceTimeBy(5000); runCurrent() } + + coVerify { relayServiceMock.subscribeRequest(any()) } + } + + @Test + fun `test batch subscribe success`() = testScope.runTest { + val topics = listOf("testTopic") + val expectedId = 123L + val relayDto = RelayDTO.BatchSubscribe.Result.Acknowledgement(id = expectedId, result = listOf("testId")) + + coEvery { relayServiceMock.batchSubscribeRequest(any()) } returns Unit + coEvery { relayServiceMock.observeBatchSubscribeAcknowledgement() } returns flowOf(relayDto) + + client.observeResults() + client.connectionState.value = ConnectionState.Open + client.batchSubscribe(topics, expectedId) { result -> + result.fold( + onSuccess = { + assertEquals(expectedId, result.getOrNull()?.id) + }, + onFailure = { fail(it.message) } + ) + } + + coVerify { relayServiceMock.batchSubscribeRequest(any()) } + } + + @Test + fun `test batch subscribe failure due to timeout`() = testScope.runTest { + val topics = listOf("testTopic") + + coEvery { relayServiceMock.batchSubscribeRequest(any()) } returns Unit + coEvery { relayServiceMock.observeBatchSubscribeAcknowledgement() } returns flow { delay(10000L) } + + client.batchSubscribe(topics) { result -> + result.fold( + onSuccess = { + fail("Should not be successful") + }, + onFailure = { + assertTrue(result.exceptionOrNull() is TimeoutCancellationException) + } + ) + + } + + testScheduler.apply { advanceTimeBy(5000); runCurrent() } + + coVerify { relayServiceMock.batchSubscribeRequest(any()) } + } + + @Test + fun `test unsubscribe success`() = testScope.runTest { + val topic = "testTopic" + val expectedId = 123L + val relayDto = RelayDTO.Unsubscribe.Result.Acknowledgement(id = expectedId, result = true) + + coEvery { relayServiceMock.unsubscribeRequest(any()) } returns Unit + coEvery { relayServiceMock.observeUnsubscribeAcknowledgement() } returns flowOf(relayDto) + + client.observeResults() + client.connectionState.value = ConnectionState.Open + client.unsubscribe(topic, "subsId", expectedId) { result -> + result.fold( + onSuccess = { + assertEquals(expectedId, result.getOrNull()?.id) + }, + onFailure = { fail(it.message) } + ) + } + + coVerify { relayServiceMock.unsubscribeRequest(any()) } + } + + @Test + fun `test unsubscribe failure`() = testScope.runTest { + val topic = "testTopic" + + coEvery { relayServiceMock.subscribeRequest(any()) } returns Unit + coEvery { relayServiceMock.observeSubscribeAcknowledgement() } returns flow { delay(10000L) } + + client.connectionState.value = ConnectionState.Open + client.subscribe(topic) { result -> + result.fold( + onSuccess = { + fail("Should not be successful") + }, + onFailure = { + assertTrue(result.exceptionOrNull() is TimeoutCancellationException) + } + ) + } + + testScheduler.apply { advanceTimeBy(5000); runCurrent() } + + coVerify { relayServiceMock.subscribeRequest(any()) } + } } \ No newline at end of file diff --git a/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCaseTest.kt b/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCaseTest.kt index 58b18c6da..091d511f0 100644 --- a/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCaseTest.kt +++ b/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCaseTest.kt @@ -168,7 +168,7 @@ class ApproveSessionAuthenticateUseCaseTest { assert(throwable is MissingSessionAuthenticateRequest) }) - coVerify { insertEventUseCase(any()) } + coVerify { insertTelemetryEventUseCase(any()) } } @Test @@ -213,7 +213,7 @@ class ApproveSessionAuthenticateUseCaseTest { assert(throwable is RequestExpiredException) }) - coVerify { insertEventUseCase(any()) } + coVerify { insertTelemetryEventUseCase(any()) } } @Test