diff --git a/core/android/build.gradle.kts b/core/android/build.gradle.kts index e0763d4af8..11f2df87b0 100644 --- a/core/android/build.gradle.kts +++ b/core/android/build.gradle.kts @@ -110,6 +110,7 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.bundles.scarlet.test) testImplementation(libs.bundles.sqlDelight.test) + testImplementation(libs.koin.test) androidTestUtil(libs.androidx.testOrchestrator) androidTestImplementation(libs.bundles.androidxAndroidTest) diff --git a/core/android/src/androidTest/kotlin/com/walletconnect/android/di/OverrideModule.kt b/core/android/src/androidTest/kotlin/com/walletconnect/android/di/OverrideModule.kt index 7c18097d82..c01c0a28e9 100644 --- a/core/android/src/androidTest/kotlin/com/walletconnect/android/di/OverrideModule.kt +++ b/core/android/src/androidTest/kotlin/com/walletconnect/android/di/OverrideModule.kt @@ -13,14 +13,20 @@ private const val KEY_STORE_ALIAS = "wc_keystore_key" // When called more that once, different `storagePrefix` must be defined. @JvmSynthetic -internal fun overrideModule(relay: RelayConnectionInterface, pairing: PairingInterface, pairingController: PairingControllerInterface, storagePrefix: String) = module { +internal fun overrideModule( + relay: RelayConnectionInterface, + pairing: PairingInterface, + pairingController: PairingControllerInterface, + storagePrefix: String, + bundleId: String +) = module { val sharedPrefsFile = storagePrefix + SHARED_PREFS_FILE val keyStoreAlias = storagePrefix + KEY_STORE_ALIAS single { relay } includes( - coreStorageModule(storagePrefix), + coreStorageModule(storagePrefix, bundleId), corePairingModule(pairing, pairingController), coreCryptoModule(sharedPrefsFile, keyStoreAlias), coreJsonRpcModule() diff --git a/core/android/src/androidTest/kotlin/com/walletconnect/android/test/utils/TestClient.kt b/core/android/src/androidTest/kotlin/com/walletconnect/android/test/utils/TestClient.kt index 203fe0fbd4..86865d856e 100644 --- a/core/android/src/androidTest/kotlin/com/walletconnect/android/test/utils/TestClient.kt +++ b/core/android/src/androidTest/kotlin/com/walletconnect/android/test/utils/TestClient.kt @@ -78,7 +78,7 @@ internal object TestClient { Relay = RelayClient(secondaryKoinApp) // Override of storage instances and depending objects - secondaryKoinApp.modules(overrideModule(Relay, Pairing, PairingController, "test_secondary")) + secondaryKoinApp.modules(overrideModule(Relay, Pairing, PairingController, "test_secondary", app.packageName)) // Necessary reinit of Relay, Pairing and PairingController Relay.initialize { Timber.e(it) } diff --git a/core/android/src/debug/kotlin/com/walletconnect/android/di/CoreStorageModule.kt b/core/android/src/debug/kotlin/com/walletconnect/android/di/CoreStorageModule.kt index d3899f4dd3..53084c3bad 100644 --- a/core/android/src/debug/kotlin/com/walletconnect/android/di/CoreStorageModule.kt +++ b/core/android/src/debug/kotlin/com/walletconnect/android/di/CoreStorageModule.kt @@ -12,9 +12,9 @@ import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named import org.koin.dsl.module -fun coreStorageModule(storagePrefix: String = String.Empty) = module { +fun coreStorageModule(storagePrefix: String = String.Empty, bundleId: String) = module { - includes(baseStorageModule(storagePrefix)) + includes(baseStorageModule(storagePrefix, bundleId)) single(named(AndroidBuildVariantDITags.ANDROID_CORE_DATABASE_DRIVER)) { AndroidSqliteDriver( diff --git a/core/android/src/main/kotlin/com/walletconnect/android/CoreInterface.kt b/core/android/src/main/kotlin/com/walletconnect/android/CoreInterface.kt index 1fb48f6a6f..76c531fbee 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/CoreInterface.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/CoreInterface.kt @@ -32,6 +32,7 @@ interface CoreInterface { relay: RelayConnectionInterface? = null, keyServerUrl: String? = null, networkClientTimeout: NetworkClientTimeout? = null, + telemetryEnabled: Boolean = true, onError: (Core.Model.Error) -> Unit, ) } \ No newline at end of file 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 0cad4d3467..2b507ffe1e 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/CoreProtocol.kt @@ -2,6 +2,7 @@ package com.walletconnect.android import android.app.Application import com.walletconnect.android.di.coreStorageModule +import com.walletconnect.android.internal.common.di.AndroidCommonDITags import com.walletconnect.android.internal.common.di.coreAndroidNetworkModule import com.walletconnect.android.internal.common.di.coreCommonModule import com.walletconnect.android.internal.common.di.coreCryptoModule @@ -17,6 +18,7 @@ import com.walletconnect.android.internal.common.explorer.ExplorerProtocol import com.walletconnect.android.internal.common.model.AppMetaData import com.walletconnect.android.internal.common.model.ProjectId import com.walletconnect.android.internal.common.model.Redirect +import com.walletconnect.android.internal.common.model.TelemetryEnabled import com.walletconnect.android.internal.common.wcKoinApp import com.walletconnect.android.pairing.client.PairingInterface import com.walletconnect.android.pairing.client.PairingProtocol @@ -36,6 +38,7 @@ import com.walletconnect.android.verify.client.VerifyClient import com.walletconnect.android.verify.client.VerifyInterface import org.koin.android.ext.koin.androidContext import org.koin.core.KoinApplication +import org.koin.core.qualifier.named import org.koin.dsl.module class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInterface { @@ -69,6 +72,7 @@ class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInter relay: RelayConnectionInterface?, keyServerUrl: String?, networkClientTimeout: NetworkClientTimeout?, + telemetryEnabled: Boolean, onError: (Core.Model.Error) -> Unit ) { try { @@ -78,6 +82,7 @@ class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInter require(relayServerUrl.isValidRelayServerUrl()) { "Check the schema and projectId parameter of the Server Url" } modules( module { single { ProjectId(relayServerUrl.projectId()) } }, + module { single(named(AndroidCommonDITags.TELEMETRY_ENABLED)) { TelemetryEnabled(telemetryEnabled) } }, coreAndroidNetworkModule(relayServerUrl, connectionType.toCommonConnectionType(), BuildConfig.SDK_VERSION, networkClientTimeout, bundleId), coreCommonModule(), coreCryptoModule(), @@ -88,7 +93,7 @@ class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInter } modules( - coreStorageModule(), + coreStorageModule(bundleId = bundleId), pushModule(), module { single { relay ?: Relay } }, module { single { with(metaData) { AppMetaData(name = name, description = description, url = url, icons = icons, redirect = Redirect(redirect)) } } }, 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 735d30527f..4a52f2104c 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 @@ -39,6 +39,7 @@ enum class AndroidCommonDITags { DECRYPT_AUTH_MESSAGE, DECRYPT_NOTIFY_MESSAGE, DECRYPT_USE_CASES, - ENABLE_ANALYTICS, - ENABLE_AUTHENTICATE + ENABLE_WEB_3_MODAL_ANALYTICS, + ENABLE_AUTHENTICATE, + TELEMETRY_ENABLED, } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/BaseStorageModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/BaseStorageModule.kt index d9c68d2f3c..c945fe0bfa 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/BaseStorageModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/BaseStorageModule.kt @@ -6,6 +6,7 @@ import com.squareup.moshi.Moshi import com.walletconnect.android.di.AndroidBuildVariantDITags import com.walletconnect.android.internal.common.model.AppMetaDataType import com.walletconnect.android.internal.common.model.Validation +import com.walletconnect.android.internal.common.storage.events.EventsRepository import com.walletconnect.android.internal.common.storage.identity.IdentitiesStorageRepository import com.walletconnect.android.internal.common.storage.metadata.MetadataStorageRepository import com.walletconnect.android.internal.common.storage.metadata.MetadataStorageRepositoryInterface @@ -15,6 +16,7 @@ import com.walletconnect.android.internal.common.storage.push_messages.PushMessa import com.walletconnect.android.internal.common.storage.rpc.JsonRpcHistory import com.walletconnect.android.internal.common.storage.verify.VerifyContextStorageRepository import com.walletconnect.android.sdk.core.AndroidCoreDatabase +import com.walletconnect.android.sdk.storage.data.dao.EventDao import com.walletconnect.android.sdk.storage.data.dao.MetaData import com.walletconnect.android.sdk.storage.data.dao.VerifyContext import com.walletconnect.utils.Empty @@ -24,7 +26,7 @@ import org.koin.core.scope.Scope import org.koin.dsl.module import com.walletconnect.android.internal.common.scope as wcScope -fun baseStorageModule(storagePrefix: String = String.Empty) = module { +fun baseStorageModule(storagePrefix: String = String.Empty, bundleId: String) = module { fun Scope.createCoreDB(): AndroidCoreDatabase = AndroidCoreDatabase( driver = get(named(AndroidBuildVariantDITags.ANDROID_CORE_DATABASE_DRIVER)), @@ -34,6 +36,9 @@ fun baseStorageModule(storagePrefix: String = String.Empty) = module { ), VerifyContextAdapter = VerifyContext.Adapter( validationAdapter = get(named(AndroidCommonDITags.COLUMN_ADAPTER_VALIDATION)) + ), + EventDaoAdapter = EventDao.Adapter( + traceAdapter = get(named(AndroidCommonDITags.COLUMN_ADAPTER_LIST)) ) ) @@ -101,6 +106,8 @@ fun baseStorageModule(storagePrefix: String = String.Empty) = module { single { get(named(AndroidBuildVariantDITags.ANDROID_CORE_DATABASE)).pushMessageQueries } + single { get(named(AndroidBuildVariantDITags.ANDROID_CORE_DATABASE)).eventQueries } + single { MetadataStorageRepository(metaDataQueries = get()) } single { PairingStorageRepository(pairingQueries = get()) } @@ -113,5 +120,7 @@ fun baseStorageModule(storagePrefix: String = String.Empty) = module { single { PushMessagesRepository(pushMessageQueries = get()) } + single { EventsRepository(eventQueries = get(), bundleId = bundleId, telemetryEnabled = get(named(AndroidCommonDITags.TELEMETRY_ENABLED))) } + single { DatabaseConfig(storagePrefix = storagePrefix) } } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreCommonModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreCommonModule.kt index d4366d836e..092f4b2f07 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreCommonModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/CoreCommonModule.kt @@ -28,25 +28,6 @@ fun coreCommonModule() = module { .withSubtype(JsonRpcResponse.JsonRpcError::class.java, "error") } - single> { - PolymorphicJsonAdapterFactory.of(Props::class.java, "type") - .withSubtype(Props.ModalCreated::class.java, "modal_created") - .withSubtype(Props.ModalLoaded::class.java, "modal_loaded") - .withSubtype(Props.ModalOpen::class.java, "modal_open") - .withSubtype(Props.ModalClose::class.java, "modal_close") - .withSubtype(Props.ClickNetworks::class.java, "click_networks") - .withSubtype(Props.ClickAllWallets::class.java, "click_all_wallets") - .withSubtype(Props.SwitchNetwork::class.java, "switch_network") - .withSubtype(Props.SelectWallet::class.java, "select_wallet") - .withSubtype(Props.ConnectSuccess::class.java, "connect_success") - .withSubtype(Props.ConnectError::class.java, "connect_error") - .withSubtype(Props.DisconnectSuccess::class.java, "disconnect_success") - .withSubtype(Props.DisconnectError::class.java, "disconnect_error") - .withSubtype(Props.ClickWalletHelp::class.java, "click_wallet_help") - .withSubtype(Props.ClickNetworkHelp::class.java, "click_network_help") - .withSubtype(Props.ClickGetWallet::class.java, "click_get_wallet") - } - single(named(AndroidCommonDITags.MOSHI)) { get(named(FoundationDITags.MOSHI)) .newBuilder() 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 01580247e1..0c3fad1d6c 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 @@ -14,7 +14,9 @@ fun corePairingModule(pairing: PairingInterface, pairingController: PairingContr metadataRepository = get(), pairingRepository = get(), jsonRpcInteractor = get(), - logger = get(named(AndroidCommonDITags.LOGGER)) + logger = get(named(AndroidCommonDITags.LOGGER)), + insertEventUseCase = get(), + sendBatchEventUseCase = get(), ) } single { pairing } diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/PulseModule.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/PulseModule.kt index 6d25eddfcf..8814513cfa 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/PulseModule.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/di/PulseModule.kt @@ -1,22 +1,12 @@ package com.walletconnect.android.internal.common.di +import com.squareup.moshi.Moshi +import com.walletconnect.android.internal.common.model.TelemetryEnabled import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.domain.SendClickAllWalletsUseCase -import com.walletconnect.android.pulse.domain.SendClickGetWalletUseCase -import com.walletconnect.android.pulse.domain.SendClickNetworkHelpUseCase -import com.walletconnect.android.pulse.domain.SendClickNetworksUseCase -import com.walletconnect.android.pulse.domain.SendClickWalletHelpUseCase -import com.walletconnect.android.pulse.domain.SendConnectErrorUseCase -import com.walletconnect.android.pulse.domain.SendConnectSuccessUseCase -import com.walletconnect.android.pulse.domain.SendDisconnectErrorUseCase -import com.walletconnect.android.pulse.domain.SendDisconnectSuccessUseCase -import com.walletconnect.android.pulse.domain.SendModalCloseUseCase -import com.walletconnect.android.pulse.domain.SendModalCreatedUseCase -import com.walletconnect.android.pulse.domain.SendModalLoadedUseCase -import com.walletconnect.android.pulse.domain.SendModalLoadedUseCaseInterface -import com.walletconnect.android.pulse.domain.SendModalOpenUseCase -import com.walletconnect.android.pulse.domain.SendSelectWalletUseCase -import com.walletconnect.android.pulse.domain.SendSwitchNetworkUseCase +import com.walletconnect.android.pulse.domain.InsertEventUseCase +import com.walletconnect.android.pulse.domain.SendBatchEventUseCase +import com.walletconnect.android.pulse.domain.SendEventInterface +import com.walletconnect.android.pulse.domain.SendEventUseCase import org.koin.core.qualifier.named import org.koin.dsl.module import retrofit2.Retrofit @@ -30,38 +20,16 @@ fun pulseModule(bundleId: String) = module { Retrofit.Builder() .baseUrl(get(named(AndroidCommonDITags.PULSE_URL))) .client(get(named(AndroidCommonDITags.WEB3MODAL_OKHTTP))) - .addConverterFactory(MoshiConverterFactory.create(get(named(AndroidCommonDITags.MOSHI)))) + .addConverterFactory(MoshiConverterFactory.create(get(named(AndroidCommonDITags.MOSHI)).build())) .build() } - single { get(named(AndroidCommonDITags.PULSE_RETROFIT)).create(PulseService::class.java) } - - single { - SendModalCreatedUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendClickAllWalletsUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - single { - SendClickGetWalletUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) + get(named(AndroidCommonDITags.PULSE_RETROFIT)).create(PulseService::class.java) } - single { - SendClickWalletHelpUseCase( + single { + SendEventUseCase( pulseService = get(), logger = get(named(AndroidCommonDITags.LOGGER)), bundleId = bundleId @@ -69,90 +37,18 @@ fun pulseModule(bundleId: String) = module { } single { - SendClickNetworkHelpUseCase( + SendBatchEventUseCase( pulseService = get(), logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId + telemetryEnabled = get(named(AndroidCommonDITags.TELEMETRY_ENABLED)), + eventsRepository = get(), ) } single { - SendClickNetworksUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendConnectErrorUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendConnectSuccessUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendDisconnectErrorUseCase( - pulseService = get(), + InsertEventUseCase( logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendDisconnectSuccessUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendModalCloseUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendModalLoadedUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendModalOpenUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendSelectWalletUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId - ) - } - - single { - SendSwitchNetworkUseCase( - pulseService = get(), - logger = get(named(AndroidCommonDITags.LOGGER)), - bundleId = bundleId + eventsRepository = get(), ) } } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/TelemetryEnabled.kt b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/TelemetryEnabled.kt new file mode 100644 index 0000000000..3444635762 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/model/TelemetryEnabled.kt @@ -0,0 +1,4 @@ +package com.walletconnect.android.internal.common.model + +@JvmInline +value class TelemetryEnabled(val value: Boolean) \ No newline at end of file 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 new file mode 100644 index 0000000000..14ce681802 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/events/EventsRepository.kt @@ -0,0 +1,71 @@ +package com.walletconnect.android.internal.common.storage.events + +import android.database.sqlite.SQLiteException +import app.cash.sqldelight.async.coroutines.awaitAsList +import com.walletconnect.android.internal.common.model.TelemetryEnabled +import com.walletconnect.android.pulse.model.Event +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.android.sdk.storage.data.dao.EventQueries +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class EventsRepository( + private val eventQueries: EventQueries, + private val bundleId: String, + private val telemetryEnabled: TelemetryEnabled, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + @Throws(SQLiteException::class) + suspend fun insertOrAbort(props: Props) = withContext(dispatcher) { + if (telemetryEnabled.value) { + with(Event(bundleId = bundleId, props = props)) { + eventQueries.insertOrAbort( + eventId, + bundleId, + timestamp, + this.props.event, + this.props.type, + this.props.properties?.topic, + this.props.properties?.trace + ) + } + } + } + + @Throws(SQLiteException::class) + suspend fun getAllWithLimitAndOffset(limit: Int, offset: Int): List { + return eventQueries.getAllWithLimitAndOffset(limit.toLong(), offset.toLong()) + .awaitAsList() + .map { + Event( + eventId = it.event_id, + bundleId = it.bundle_id, + timestamp = it.timestamp, + props = Props( + event = it.event_name, + type = it.type, + properties = Properties( + topic = it.topic, + trace = it.trace + ) + ) + ) + } + } + + @Throws(SQLiteException::class) + suspend fun deleteAll() { + return withContext(dispatcher) { + eventQueries.deleteAll() + } + } + + @Throws(SQLiteException::class) + suspend fun deleteByIds(eventIds: List) { + withContext(dispatcher) { + eventQueries.deleteByIds(eventIds) + } + } +} \ 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 7cb0cb5296..674d8f9c87 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 @@ -9,6 +9,9 @@ import com.walletconnect.android.internal.common.wcKoinApp 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.InsertEventUseCase +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 @@ -23,6 +26,7 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) private lateinit var pairingEngine: PairingEngine private val logger: Logger by lazy { koinApp.koin.get() } private val relayClient: RelayConnectionInterface by lazy { koinApp.koin.get() } + private val insertEventUseCase: InsertEventUseCase by lazy { koinApp.koin.get() } override fun initialize() { pairingEngine = koinApp.koin.get() @@ -153,6 +157,7 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) return@withTimeout } } else { + insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION)) errorLambda(Throwable("No internet connection")) return@withTimeout } @@ -161,6 +166,7 @@ internal class PairingProtocol(private val koinApp: KoinApplication = wcKoinApp) } } } catch (e: Exception) { + insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION)) errorLambda(Throwable("Failed to connect: ${e.message}")) } } 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 e08c765cfc..9120f6dc8f 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 @@ -11,6 +11,8 @@ import com.walletconnect.android.internal.common.exception.CannotFindSequenceFor import com.walletconnect.android.internal.common.exception.ExpiredPairingException import com.walletconnect.android.internal.common.exception.Invalid import com.walletconnect.android.internal.common.exception.MalformedWalletConnectUri +import com.walletconnect.android.internal.common.exception.NoInternetConnectionException +import com.walletconnect.android.internal.common.exception.NoRelayConnectionException import com.walletconnect.android.internal.common.exception.PairWithExistingPairingIsNotAllowed import com.walletconnect.android.internal.common.exception.Uncategorized import com.walletconnect.android.internal.common.model.AppMetaData @@ -38,6 +40,12 @@ import com.walletconnect.android.pairing.model.PairingParams import com.walletconnect.android.pairing.model.PairingRpc import com.walletconnect.android.pairing.model.inactivePairing import com.walletconnect.android.pairing.model.mapper.toCore +import com.walletconnect.android.pulse.domain.InsertEventUseCase +import com.walletconnect.android.pulse.domain.SendBatchEventUseCase +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 @@ -76,7 +84,9 @@ internal class PairingEngine( private val metadataRepository: MetadataStorageRepositoryInterface, private val crypto: KeyManagementRepository, private val jsonRpcInteractor: JsonRpcInteractorInterface, - private val pairingRepository: PairingStorageRepositoryInterface + private val pairingRepository: PairingStorageRepositoryInterface, + private val insertEventUseCase: InsertEventUseCase, + private val sendBatchEventUseCase: SendBatchEventUseCase ) { private var jsonRpcRequestsJob: Job? = null private val setOfRegisteredMethods: MutableSet = mutableSetOf() @@ -90,8 +100,8 @@ internal class PairingEngine( merge(_engineEvent, _deletedPairingFlow.map { pairing -> EngineDO.PairingExpire(pairing) }, _isPairingStateFlow.map { EngineDO.PairingState(it) }) .shareIn(scope, SharingStarted.Lazily, 0) - private val _activePairingTopicFlow: MutableSharedFlow = MutableSharedFlow() - val activePairingTopicFlow: SharedFlow = _activePairingTopicFlow.asSharedFlow() + private val _inactivePairingTopicFlow: MutableSharedFlow>> = MutableSharedFlow() + val inactivePairingTopicFlow: SharedFlow>> = _inactivePairingTopicFlow.asSharedFlow() val internalErrorFlow = MutableSharedFlow() @@ -102,6 +112,7 @@ internal class PairingEngine( inactivePairingsExpiryWatcher() activePairingsExpiryWatcher() isPairingStateWatcher() + sendEvents() } val jsonRpcErrorFlow: Flow by lazy { @@ -149,53 +160,71 @@ internal class PairingEngine( } fun pair(uri: String, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { - val walletConnectUri: WalletConnectUri = Validator.validateWCUri(uri) ?: return onFailure(MalformedWalletConnectUri(MALFORMED_PAIRING_URI_MESSAGE)) + 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))) } } + return onFailure(MalformedWalletConnectUri(MALFORMED_PAIRING_URI_MESSAGE)) + } + trace.add(Trace.Pairing.PAIRING_URI_VALIDATION_SUCCESS) val inactivePairing = Pairing(walletConnectUri) + val pairingTopic = inactivePairing.topic val symmetricKey = walletConnectUri.symKey - try { - logger.log("Pairing started: ${inactivePairing.topic}") if (walletConnectUri.expiry?.isExpired() == true) { - logger.error("Pairing expired: ${inactivePairing.topic.value}") - return onFailure(ExpiredPairingException("Pairing expired: ${walletConnectUri.topic.value}")) + scope.launch { supervisorScope { insertEventUseCase(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")) } - if (pairingRepository.getPairingOrNullByTopic(inactivePairing.topic) != null) { - val pairing = pairingRepository.getPairingOrNullByTopic(inactivePairing.topic) + trace.add(Trace.Pairing.PAIRING_URI_NOT_EXPIRED) + if (pairingRepository.getPairingOrNullByTopic(pairingTopic) != null) { + val pairing = pairingRepository.getPairingOrNullByTopic(pairingTopic) + trace.add(Trace.Pairing.EXISTING_PAIRING) if (!pairing!!.isNotExpired()) { - logger.error("Pairing expired: ${inactivePairing.topic.value}") - return onFailure(ExpiredPairingException("Pairing expired: ${pairing.topic.value}")) + scope.launch { supervisorScope { insertEventUseCase(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}")) } + trace.add(Trace.Pairing.PAIRING_NOT_EXPIRED) if (pairing.isActive) { - logger.error("Pairing already exists error: ${inactivePairing.topic.value}") + scope.launch { supervisorScope { insertEventUseCase(Props(type = EventType.Error.PAIRING_ALREADY_EXIST, properties = Properties(trace = trace, topic = pairingTopic.value))) } } + .also { logger.error("Pairing already exists error: $pairingTopic") } return onFailure(PairWithExistingPairingIsNotAllowed(PAIRING_NOT_ALLOWED_MESSAGE)) } else { - logger.log("Emitting activate pairing: ${inactivePairing.topic.value}") + trace.add(Trace.Pairing.EMIT_INACTIVE_PAIRING).also { logger.log("Emitting inactive pairing: $pairingTopic") } scope.launch { supervisorScope { - _activePairingTopicFlow.emit(inactivePairing.topic) + _inactivePairingTopicFlow.emit(Pair(pairingTopic, trace)) } } } } else { - crypto.setKey(symmetricKey, walletConnectUri.topic.value) + crypto.setKey(symmetricKey, pairingTopic.value) pairingRepository.insertPairing(inactivePairing) + trace.add(Trace.Pairing.STORE_NEW_PAIRING).also { logger.log("Storing a new pairing: $pairingTopic") } } - - logger.log("Subscribing pairing topic: ${inactivePairing.topic.value}") - jsonRpcInteractor.subscribe(topic = inactivePairing.topic, + trace.add(Trace.Pairing.SUBSCRIBING_PAIRING_TOPIC).also { logger.log("Subscribing pairing topic: $pairingTopic") } + jsonRpcInteractor.subscribe(topic = pairingTopic, onSuccess = { - logger.log("Subscribe pairing topic success: ${inactivePairing.topic.value}") + trace.add(Trace.Pairing.SUBSCRIBE_PAIRING_TOPIC_SUCCESS).also { logger.log("Subscribe pairing topic success: $pairingTopic") } onSuccess() }, onFailure = { error -> - logger.error("Subscribe pairing topic error: ${inactivePairing.topic.value}, error: $error") + scope.launch { + supervisorScope { + insertEventUseCase(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) - }) + } + ) } catch (e: Exception) { - logger.error("Subscribe pairing topic error: ${inactivePairing.topic.value}, error: $e") - runCatching { - crypto.removeKeys(walletConnectUri.topic.value) - }.onFailure { logger.error("Remove keys error: ${inactivePairing.topic.value}, error: $it") } - jsonRpcInteractor.unsubscribe(inactivePairing.topic) + 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))) } } + if (e is NoInternetConnectionException) + scope.launch { supervisorScope { insertEventUseCase(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) } } @@ -272,6 +301,18 @@ internal class PairingEngine( metadataRepository.upsertPeerMetadata(Topic(topic), metadata, metaDataType) } + private fun sendEvents() { + scope.launch { + supervisorScope { + try { + sendBatchEventUseCase() + } catch (e: Exception) { + logger.error("Error when sending events: $e") + } + } + } + } + private fun resubscribeToPairingTopics() { jsonRpcInteractor.wssConnectionState .filterIsInstance() diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingController.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingController.kt index b58c1b2020..07acd74154 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingController.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingController.kt @@ -16,7 +16,7 @@ internal class PairingController(private val koinApp: KoinApplication = wcKoinAp private lateinit var pairingEngine: PairingEngine override val deletedPairingFlow: SharedFlow by lazy { pairingEngine.deletedPairingFlow } override val findWrongMethodsFlow: Flow by lazy { merge(pairingEngine.internalErrorFlow, pairingEngine.jsonRpcErrorFlow) } - override val activePairingFlow: SharedFlow by lazy { pairingEngine.activePairingTopicFlow } + override val inactivePairingFlow: SharedFlow>> by lazy { pairingEngine.inactivePairingTopicFlow } override fun initialize() { pairingEngine = koinApp.koin.get() diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingControllerInterface.kt b/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingControllerInterface.kt index dab2e9a5e1..08bb714547 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingControllerInterface.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pairing/handler/PairingControllerInterface.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.SharedFlow interface PairingControllerInterface { val deletedPairingFlow: SharedFlow val findWrongMethodsFlow: Flow - val activePairingFlow: SharedFlow + val inactivePairingFlow: SharedFlow>> fun initialize() diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/data/PulseService.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/data/PulseService.kt index bcc135978e..d3e13c6662 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/data/PulseService.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/data/PulseService.kt @@ -12,5 +12,9 @@ import retrofit2.http.POST interface PulseService { @Headers("Content-Type: application/json") @POST("/e") - suspend fun sendEvent(@Header("x-sdk-type") sdkType: String = "w3m", @Body body: Event): Response + suspend fun sendEvent(@Header("x-sdk-type") sdkType: String, @Body body: Event): Response + + @Headers("Content-Type: application/json") + @POST("/batch") + suspend fun sendEventBatch(@Header("x-sdk-type") sdkType: String, @Body body: List): Response } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/InsertEventUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/InsertEventUseCase.kt new file mode 100644 index 0000000000..2d321b28df --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/InsertEventUseCase.kt @@ -0,0 +1,22 @@ +package com.walletconnect.android.pulse.domain + +import com.walletconnect.android.internal.common.storage.events.EventsRepository +import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.foundation.util.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class InsertEventUseCase( + private val eventsRepository: EventsRepository, + private val logger: Logger +) { + suspend operator fun invoke(props: Props) { + withContext(Dispatchers.IO) { + try { + eventsRepository.insertOrAbort(props) + } catch (e: Exception) { + logger.error("Inserting event ${props.type} error: $e") + } + } + } +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendBatchEventUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendBatchEventUseCase.kt new file mode 100644 index 0000000000..88e5b7acc5 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendBatchEventUseCase.kt @@ -0,0 +1,52 @@ +package com.walletconnect.android.pulse.domain + +import com.walletconnect.android.internal.common.model.TelemetryEnabled +import com.walletconnect.android.internal.common.storage.events.EventsRepository +import com.walletconnect.android.pulse.data.PulseService +import com.walletconnect.android.pulse.model.SDKType +import com.walletconnect.foundation.util.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SendBatchEventUseCase( + private val pulseService: PulseService, + private val eventsRepository: EventsRepository, + private val telemetryEnabled: TelemetryEnabled, + private val logger: Logger, +) { + suspend operator fun invoke() = withContext(Dispatchers.IO) { + if (telemetryEnabled.value) { + var continueProcessing = true + while (continueProcessing) { + val events = eventsRepository.getAllWithLimitAndOffset(LIMIT, 0) + if (events.isNotEmpty()) { + try { + logger.log("Sending batch events: ${events.size}") + val response = pulseService.sendEventBatch(body = events, sdkType = SDKType.EVENTS.type) + if (response.isSuccessful) { + eventsRepository.deleteByIds(events.map { it.eventId }) + } else { + logger.log("Failed to send events: ${events.size}") + continueProcessing = false + } + } catch (e: Exception) { + logger.error("Error sending batch events: ${e.message}") + continueProcessing = false + } + } else { + continueProcessing = false + } + } + } else { + try { + eventsRepository.deleteAll() + } catch (e: Exception) { + logger.error("Failed to delete events, error: $e") + } + } + } + + companion object { + private const val LIMIT = 500 + } +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickAllWalletsUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickAllWalletsUseCase.kt deleted file mode 100644 index 97b0a9910e..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickAllWalletsUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendClickAllWalletsUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ClickAllWallets() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickGetWalletUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickGetWalletUseCase.kt deleted file mode 100644 index 380d5d812d..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickGetWalletUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendClickGetWalletUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ClickGetWallet() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickNetworkHelpUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickNetworkHelpUseCase.kt deleted file mode 100644 index 6d7693a826..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickNetworkHelpUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendClickNetworkHelpUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ClickNetworkHelp() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickNetworksUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickNetworksUseCase.kt deleted file mode 100644 index e58e760929..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickNetworksUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendClickNetworksUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ClickNetworks() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickWalletHelpUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickWalletHelpUseCase.kt deleted file mode 100644 index 5c465168bb..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendClickWalletHelpUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendClickWalletHelpUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ClickWalletHelp() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendConnectErrorUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendConnectErrorUseCase.kt deleted file mode 100644 index f14388a62d..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendConnectErrorUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.ConnectErrorProperties -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendConnectErrorUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke(message: String) { - val properties = ConnectErrorProperties(message) - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ConnectError(properties = properties) - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendConnectSuccessUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendConnectSuccessUseCase.kt deleted file mode 100644 index d719c5fd59..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendConnectSuccessUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.ConnectSuccessProperties -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendConnectSuccessUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke(name: String, method: String) { - val properties = ConnectSuccessProperties(name, method) - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ConnectSuccess(properties = properties) - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendDisconnectErrorUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendDisconnectErrorUseCase.kt deleted file mode 100644 index c249deb974..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendDisconnectErrorUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendDisconnectErrorUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.DisconnectError() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendDisconnectSuccessUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendDisconnectSuccessUseCase.kt deleted file mode 100644 index f6d0642f1c..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendDisconnectSuccessUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendDisconnectSuccessUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.DisconnectSuccess() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendEventUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendEventUseCase.kt index 0931b23457..ec7aa81ee5 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendEventUseCase.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendEventUseCase.kt @@ -3,36 +3,45 @@ package com.walletconnect.android.pulse.domain import com.walletconnect.android.internal.common.di.AndroidCommonDITags import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.wcKoinApp +import com.walletconnect.android.internal.utils.currentTimeInSeconds import com.walletconnect.android.pulse.data.PulseService import com.walletconnect.android.pulse.model.Event +import com.walletconnect.android.pulse.model.SDKType +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.foundation.util.Logger +import com.walletconnect.util.generateId import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import org.koin.core.qualifier.named -abstract class SendEventUseCase( +class SendEventUseCase( private val pulseService: PulseService, private val logger: Logger, - internal val bundleId: String -) { - private val enableAnalytics: Boolean by lazy { wcKoinApp.koin.get(named(AndroidCommonDITags.ENABLE_ANALYTICS)) } + private val bundleId: String, +) : SendEventInterface { + private val enableW3MAnalytics: Boolean by lazy { wcKoinApp.koin.get(named(AndroidCommonDITags.ENABLE_WEB_3_MODAL_ANALYTICS)) } - operator fun invoke(event: Event) { - if (enableAnalytics) { + override fun send(props: Props, sdkType: SDKType, timestamp: Long?, id: Long?) { + if (enableW3MAnalytics) { scope.launch { supervisorScope { try { - val response = pulseService.sendEvent(body = event) + val event = Event(props = props, bundleId = bundleId, timestamp = timestamp ?: currentTimeInSeconds, eventId = id ?: generateId()) + val response = pulseService.sendEvent(body = event, sdkType = sdkType.type) if (!response.isSuccessful) { logger.error("Failed to send event: ${event.props.type}") } else { logger.log("Event sent successfully: ${event.props.type}") } } catch (e: Exception) { - logger.error("Failed to send event: ${event.props.type}, error: $e") + logger.error("Failed to send event: ${props.type}, error: $e") } } } } } +} + +interface SendEventInterface { + fun send(props: Props, sdkType: SDKType = SDKType.WEB3MODAL, timestamp: Long? = currentTimeInSeconds, id: Long? = generateId()) } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalCloseUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalCloseUseCase.kt deleted file mode 100644 index fd7926602c..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalCloseUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.ModalConnectedProperties -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendModalCloseUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke(connected: Boolean) { - val properties = ModalConnectedProperties(connected) - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ModalClose(properties = properties) - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalCreatedUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalCreatedUseCase.kt deleted file mode 100644 index 524218465b..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalCreatedUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendModalCreatedUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ModalCreated() - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalLoadedUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalLoadedUseCase.kt deleted file mode 100644 index 107a0edcd2..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalLoadedUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendModalLoadedUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendModalLoadedUseCaseInterface, SendEventUseCase(pulseService, logger, bundleId) { - - override fun sendModalLoadedEvent() { - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ModalLoaded() - ) - ) - } -} - -interface SendModalLoadedUseCaseInterface { - fun sendModalLoadedEvent() -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalOpenUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalOpenUseCase.kt deleted file mode 100644 index dc333abf38..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendModalOpenUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.ModalConnectedProperties -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendModalOpenUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke(connected: Boolean) { - val properties = ModalConnectedProperties(connected) - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.ModalOpen(properties = properties) - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendSelectWalletUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendSelectWalletUseCase.kt deleted file mode 100644 index b108cca4f9..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendSelectWalletUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.android.pulse.model.properties.SelectWalletProperties -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendSelectWalletUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke(name: String, platform: String) { - val properties = SelectWalletProperties(name, platform) - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.SelectWallet(properties = properties) - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendSwitchNetworkUseCase.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendSwitchNetworkUseCase.kt deleted file mode 100644 index 2bd2769a48..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/domain/SendSwitchNetworkUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.walletconnect.android.pulse.domain - -import com.walletconnect.android.internal.utils.currentTimeInSeconds -import com.walletconnect.android.pulse.data.PulseService -import com.walletconnect.android.pulse.model.Event -import com.walletconnect.android.pulse.model.properties.NetworkProperties -import com.walletconnect.android.pulse.model.properties.Props -import com.walletconnect.foundation.util.Logger -import com.walletconnect.util.generateId - -class SendSwitchNetworkUseCase( - pulseService: PulseService, - logger: Logger, - bundleId: String -) : SendEventUseCase(pulseService, logger, bundleId) { - operator fun invoke(network: String) { - val properties = NetworkProperties(network) - super.invoke( - Event( - eventId = generateId(), - bundleId = bundleId, - timestamp = currentTimeInSeconds, - props = Props.SwitchNetwork(properties = properties) - ) - ) - } -} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Event.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Event.kt index 601000625e..9c1fbeb703 100644 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Event.kt +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Event.kt @@ -2,16 +2,18 @@ package com.walletconnect.android.pulse.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.walletconnect.android.internal.utils.currentTimeInSeconds import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.util.generateId @JsonClass(generateAdapter = true) data class Event( @Json(name = "eventId") - val eventId: Long, + val eventId: Long = generateId(), @Json(name = "bundleId") val bundleId: String, @Json(name = "timestamp") - val timestamp: Long, + val timestamp: Long = currentTimeInSeconds, @Json(name = "props") val props: Props ) \ No newline at end of file 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 722fbd804c..d58eeeefd1 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 @@ -1,48 +1,124 @@ package com.walletconnect.android.pulse.model -internal object EventType { +object EventType { @get:JvmSynthetic - const val MODAL_CREATED: String = "MODAL_CREATED" + const val ERROR: String = "ERROR" @get:JvmSynthetic - const val MODAL_LOADED: String = "MODAL_LOADED" + const val TRACK: String = "TRACE" - @get:JvmSynthetic - const val MODAL_OPEN: String = "MODAL_OPEN" + object Error { + @get:JvmSynthetic + const val NO_WSS_CONNECTION: String = "NO_WSS_CONNECTION" - @get:JvmSynthetic - const val MODAL_CLOSE: String = "MODAL_CLOSE" + @get:JvmSynthetic + const val NO_INTERNET_CONNECTION: String = "NO_INTERNET_CONNECTION" - @get:JvmSynthetic - const val CLICK_ALL_WALLETS: String = "CLICK_ALL_WALLETS" + @get:JvmSynthetic + const val MALFORMED_PAIRING_URI: String = "MALFORMED_PAIRING_URI" - @get:JvmSynthetic - const val CLICK_NETWORKS: String = "CLICK_NETWORKS" + @get:JvmSynthetic + const val PAIRING_ALREADY_EXIST: String = "PAIRING_ALREADY_EXIST" - @get:JvmSynthetic - const val SWITCH_NETWORK: String = "SWITCH_NETWORK" + @get:JvmSynthetic + const val PAIRING_SUBSCRIPTION_FAILURE: String = "FAILED_TO_SUBSCRIBE_TO_PAIRING_TOPIC" - @get:JvmSynthetic - const val SELECT_WALLET: String = "SELECT_WALLET" + @get:JvmSynthetic + const val PAIRING_EXPIRED: String = "PAIRING_EXPIRED" - @get:JvmSynthetic - const val CONNECT_SUCCESS: String = "CONNECT_SUCCESS" + @get:JvmSynthetic + const val PROPOSAL_EXPIRED: String = "PROPOSAL_EXPIRED" - @get:JvmSynthetic - const val CONNECT_ERROR: String = "CONNECT_ERROR" + @get:JvmSynthetic + const val SESSION_SUBSCRIPTION_FAILURE: String = "SESSION_SUBSCRIPTION_FAILURE" - @get:JvmSynthetic - const val DISCONNECT_SUCCESS: String = "DISCONNECT_SUCCESS" + @get:JvmSynthetic + const val SESSION_APPROVE_PUBLISH_FAILURE: String = "SESSION_APPROVE_PUBLISH_FAILURE" - @get:JvmSynthetic - const val DISCONNECT_ERROR: String = "DISCONNECT_ERROR" + @get:JvmSynthetic + const val SESSION_SETTLE_PUBLISH_FAILURE: String = "SESSION_SETTLE_PUBLISH_FAILURE" - @get:JvmSynthetic - const val CLICK_WALLET_HELP: String = "CLICK_WALLET_HELP" + @get:JvmSynthetic + const val SESSION_APPROVE_NAMESPACE_VALIDATION_FAILURE: String = "SESSION_APPROVE_NAMESPACE_VALIDATION_FAILURE" - @get:JvmSynthetic - const val CLICK_NETWORK_HELP: String = "CLICK_NETWORK_HELP" + @get:JvmSynthetic + const val REQUIRED_NAMESPACE_VALIDATION_FAILURE: String = "REQUIRED_NAMESPACE_VALIDATION_FAILURE" - @get:JvmSynthetic - const val CLICK_GET_WALLET: String = "CLICK_GET_WALLET" + @get:JvmSynthetic + const val OPTIONAL_NAMESPACE_VALIDATION_FAILURE: String = "OPTIONAL_NAMESPACE_VALIDATION_FAILURE" + + @get:JvmSynthetic + const val SESSION_PROPERTIES_VALIDATION_FAILURE: String = "SESSION_PROPERTIES_VALIDATION_FAILURE" + + @get:JvmSynthetic + const val MISSING_SESSION_AUTH_REQUEST: String = "MISSING_SESSION_AUTH_REQUEST" + + @get:JvmSynthetic + const val SESSION_AUTH_REQUEST_EXPIRED: String = "SESSION_AUTH_REQUEST_EXPIRED" + + @get:JvmSynthetic + const val CHAINS_CAIP2_COMPLIANT_FAILURE: String = "CHAINS_CAIP2_COMPLIANT_FAILURE" + + @get:JvmSynthetic + const val CHAINS_EVM_COMPLIANT_FAILURE: String = "CHAINS_EVM_COMPLIANT_FAILURE" + + @get:JvmSynthetic + const val INVALID_CACAO: String = "INVALID_CACAO" + + @get:JvmSynthetic + const val SUBSCRIBE_AUTH_SESSION_TOPIC_FAILURE: String = "SUBSCRIBE_AUTH_SESSION_TOPIC_FAILURE" + + @get:JvmSynthetic + const val AUTHENTICATED_SESSION_APPROVE_PUBLISH_FAILURE: String = "AUTHENTICATED_SESSION_APPROVE_PUBLISH_FAILURE" + + @get:JvmSynthetic + const val AUTHENTICATED_SESSION_EXPIRED: String = "AUTHENTICATED_SESSION_EXPIRED" + } + + object Track { + @get:JvmSynthetic + const val MODAL_CREATED: String = "MODAL_CREATED" + + @get:JvmSynthetic + const val MODAL_LOADED: String = "MODAL_LOADED" + + @get:JvmSynthetic + const val MODAL_OPEN: String = "MODAL_OPEN" + + @get:JvmSynthetic + const val MODAL_CLOSE: String = "MODAL_CLOSE" + + @get:JvmSynthetic + const val CLICK_ALL_WALLETS: String = "CLICK_ALL_WALLETS" + + @get:JvmSynthetic + const val CLICK_NETWORKS: String = "CLICK_NETWORKS" + + @get:JvmSynthetic + const val SWITCH_NETWORK: String = "SWITCH_NETWORK" + + @get:JvmSynthetic + const val SELECT_WALLET: String = "SELECT_WALLET" + + @get:JvmSynthetic + const val CONNECT_SUCCESS: String = "CONNECT_SUCCESS" + + @get:JvmSynthetic + const val CONNECT_ERROR: String = "CONNECT_ERROR" + + @get:JvmSynthetic + const val DISCONNECT_SUCCESS: String = "DISCONNECT_SUCCESS" + + @get:JvmSynthetic + const val DISCONNECT_ERROR: String = "DISCONNECT_ERROR" + + @get:JvmSynthetic + const val CLICK_WALLET_HELP: String = "CLICK_WALLET_HELP" + + @get:JvmSynthetic + const val CLICK_NETWORK_HELP: String = "CLICK_NETWORK_HELP" + + @get:JvmSynthetic + const val CLICK_GET_WALLET: String = "CLICK_GET_WALLET" + } } \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/SDKType.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/SDKType.kt new file mode 100644 index 0000000000..9539e81e04 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/SDKType.kt @@ -0,0 +1,6 @@ +package com.walletconnect.android.pulse.model + +enum class SDKType(val type: String) { + WEB3MODAL("w3m"), + EVENTS("events_sdk") +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Trace.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Trace.kt new file mode 100644 index 0000000000..2a59263c5c --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/Trace.kt @@ -0,0 +1,44 @@ +package com.walletconnect.android.pulse.model + +object Trace { + object Pairing { + const val PAIRING_STARTED = "pairing_started" + const val PAIRING_URI_VALIDATION_SUCCESS = "pairing_uri_validation_success" + const val PAIRING_URI_NOT_EXPIRED = "pairing_uri_not_expired" + const val STORE_NEW_PAIRING = "store_new_pairing" + const val EXISTING_PAIRING = "existing_pairing" + const val PAIRING_NOT_EXPIRED = "pairing_not_expired" + const val EMIT_INACTIVE_PAIRING = "emit_inactive_pairing" + const val EMIT_SESSION_PROPOSAL = "emit_session_proposal" + const val SUBSCRIBING_PAIRING_TOPIC = "subscribing_pairing_topic" + const val SUBSCRIBE_PAIRING_TOPIC_SUCCESS = "subscribe_pairing_topic_success" + } + + object Session { + const val SESSION_APPROVE_STARTED = "session_approve_started" + const val PROPOSAL_NOT_EXPIRED = "proposal_not_expired" + const val SESSION_NAMESPACE_VALIDATION_SUCCESS = "session_namespaces_validation_success" + const val CREATE_SESSION_TOPIC = "create_session_topic" + const val SUBSCRIBING_SESSION_TOPIC = "subscribing_session_topic" + const val SUBSCRIBE_SESSION_TOPIC_SUCCESS = "subscribe_session_topic_success" + const val PUBLISHING_SESSION_APPROVE = "publishing_session_approve" + const val SESSION_APPROVE_PUBLISH_SUCCESS = "session_approve_publish_success" + const val STORE_SESSION = "store_session" + const val PUBLISHING_SESSION_SETTLE = "publishing_session_settle" + const val SESSION_SETTLE_PUBLISH_SUCCESS = "session_settle_publish_success" + } + + object SessionAuthenticate { + const val SESSION_AUTHENTICATE_APPROVE_STARTED = "authenticated_session_approve_started" + const val AUTHENTICATED_SESSION_NOT_EXPIRED = "authenticated_session_not_expired" + const val CHAINS_CAIP2_COMPLIANT = "chains_caip2_compliant" + const val CHAINS_EVM_COMPLIANT = "chains_evm_compliant" + const val CREATE_AUTHENTICATED_SESSION_TOPIC = "create_authenticated_session_topic" + const val CACAOS_VERIFIED = "cacaos_verified" + const val STORE_AUTHENTICATED_SESSION = "store_authenticated_session" + const val SUBSCRIBING_AUTHENTICATED_SESSION_TOPIC = "subscribing_authenticated_session_topic" + const val SUBSCRIBE_AUTHENTICATED_SESSION_TOPIC_SUCCESS = "subscribe_authenticated_session_topic_success" + const val PUBLISHING_AUTHENTICATED_SESSION_APPROVE = "publishing_authenticated_session_approve" + const val AUTHENTICATED_SESSION_APPROVE_PUBLISH_SUCCESS = "authenticated_session_approve_publish_success" + } +} \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ConnectErrorProperties.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ConnectErrorProperties.kt deleted file mode 100644 index c19159fb19..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ConnectErrorProperties.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.walletconnect.android.pulse.model.properties - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class ConnectErrorProperties( - @Json(name = "message") - val message: String, -) diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ConnectSuccessProperties.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ConnectSuccessProperties.kt deleted file mode 100644 index 4203bc733e..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ConnectSuccessProperties.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.walletconnect.android.pulse.model.properties - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class ConnectSuccessProperties( - @Json(name = "name") - val name: String, - @Json(name = "method") - val method: String -) \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ModalConnectedProperties.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ModalConnectedProperties.kt deleted file mode 100644 index aa67aac966..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/ModalConnectedProperties.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.walletconnect.android.pulse.model.properties - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class ModalConnectedProperties( - @Json(name = "connected") - val connected: Boolean -) \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/NetworkProperties.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/NetworkProperties.kt deleted file mode 100644 index 263cd48cc7..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/NetworkProperties.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.walletconnect.android.pulse.model.properties - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class NetworkProperties( - @Json(name = "network") - val network: String -) \ No newline at end of file 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 new file mode 100644 index 0000000000..a80df69848 --- /dev/null +++ b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/Properties.kt @@ -0,0 +1,24 @@ +package com.walletconnect.android.pulse.model.properties + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Properties( + @Json(name = "message") + val message: String? = null, + @Json(name = "name") + val name: String? = null, + @Json(name = "method") + val method: String? = null, + @Json(name = "connected") + val connected: Boolean? = null, + @Json(name = "network") + val network: String? = null, + @Json(name = "platform") + val platform: String? = null, + @Json(name = "trace") + val trace: List? = null, + @Json(name = "topic") + val topic: String? = null, +) 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 31f1cbb2c1..0bfb09d8e5 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 @@ -1,142 +1,13 @@ package com.walletconnect.android.pulse.model.properties import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import com.walletconnect.android.pulse.model.EventType -sealed class Props { - abstract val event: String - abstract val type: String - - @JsonClass(generateAdapter = true) - data class ModalCreated( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.MODAL_CREATED, - ) : Props() - - @JsonClass(generateAdapter = true) - data class ModalLoaded( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.MODAL_LOADED, - ) : Props() - - @JsonClass(generateAdapter = true) - data class ModalOpen( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.MODAL_OPEN, - @Json(name = "properties") - val properties: ModalConnectedProperties - ) : Props() - - @JsonClass(generateAdapter = true) - data class ModalClose( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.MODAL_CLOSE, - @Json(name = "properties") - val properties: ModalConnectedProperties - ) : Props() - - @JsonClass(generateAdapter = true) - data class ClickNetworks( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CLICK_NETWORKS, - ) : Props() - - @JsonClass(generateAdapter = true) - data class ClickAllWallets( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CLICK_ALL_WALLETS, - ) : Props() - - @JsonClass(generateAdapter = true) - data class SwitchNetwork( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.SWITCH_NETWORK, - @Json(name = "properties") - val properties: NetworkProperties - ) : Props() - - @JsonClass(generateAdapter = true) - data class SelectWallet( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.SELECT_WALLET, - @Json(name = "properties") - val properties: SelectWalletProperties - ) : Props() - - @JsonClass(generateAdapter = true) - data class ConnectSuccess( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CONNECT_SUCCESS, - @Json(name = "properties") - val properties: ConnectSuccessProperties - ) : Props() - - @JsonClass(generateAdapter = true) - data class ConnectError( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CONNECT_ERROR, - @Json(name = "properties") - val properties: ConnectErrorProperties - ) : Props() - - @JsonClass(generateAdapter = true) - data class DisconnectSuccess( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.DISCONNECT_SUCCESS - ) : Props() - - @JsonClass(generateAdapter = true) - data class DisconnectError( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.DISCONNECT_ERROR - ) : Props() - - @JsonClass(generateAdapter = true) - data class ClickWalletHelp( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CLICK_WALLET_HELP - ) : Props() - - @JsonClass(generateAdapter = true) - data class ClickNetworkHelp( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CLICK_NETWORK_HELP - ) : Props() - - @JsonClass(generateAdapter = true) - data class ClickGetWallet( - @Json(name = "event") - override val event: String = "track", - @Json(name = "type") - override val type: String = EventType.CLICK_GET_WALLET - ) : Props() -} +data class Props( + @Json(name = "event") + val event: String = EventType.ERROR, + @Json(name = "type") + val type: String, + @Json(name = "properties") + val properties: Properties? = null +) \ No newline at end of file diff --git a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/SelectWalletProperties.kt b/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/SelectWalletProperties.kt deleted file mode 100644 index 956c84e324..0000000000 --- a/core/android/src/main/kotlin/com/walletconnect/android/pulse/model/properties/SelectWalletProperties.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.walletconnect.android.pulse.model.properties - -import com.squareup.moshi.Json - -data class SelectWalletProperties( - @Json(name = "name") - val name: String, - @Json(name = "platform") - val platform: String -) \ 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 new file mode 100644 index 0000000000..36901360ed --- /dev/null +++ b/core/android/src/main/sqldelight/com/walletconnect/android/sdk/storage/data/dao/Event.sq @@ -0,0 +1,28 @@ +import kotlin.String; +import kotlin.collections.List; + +CREATE TABLE EventDao( + event_id INTEGER PRIMARY KEY NOT NULL, + bundle_id TEXT NOT NULL, + timestamp INTEGER NOT NULL, + event_name TEXT NOT NULL, + type TEXT NOT NULL, + topic TEXT, + trace TEXT AS List +); + +insertOrAbort: +INSERT OR ABORT INTO EventDao(event_id, bundle_id, timestamp, event_name, type, topic, trace) +VALUES (?,?,?,?,?,?, ?); + +getAllWithLimitAndOffset: +SELECT event_id, bundle_id, timestamp, event_name, type, topic, trace +FROM EventDao ed +LIMIT ? OFFSET ?; + +deleteByIds: +DELETE FROM EventDao +WHERE event_id IN ?; + +deleteAll: +DELETE FROM EventDao; \ No newline at end of file diff --git a/core/android/src/main/sqldelight/databases/9.db b/core/android/src/main/sqldelight/databases/9.db new file mode 100644 index 0000000000..db6fe0c039 Binary files /dev/null and b/core/android/src/main/sqldelight/databases/9.db differ diff --git a/core/android/src/main/sqldelight/migration/8.sqm b/core/android/src/main/sqldelight/migration/8.sqm new file mode 100644 index 0000000000..1b7ae775f9 --- /dev/null +++ b/core/android/src/main/sqldelight/migration/8.sqm @@ -0,0 +1,14 @@ +import com.sun.tools.javac.util.List; +import kotlin.String; + +-- migration from 8.db to 9.db + +CREATE TABLE EventDao( + event_id INTEGER PRIMARY KEY NOT NULL, + bundle_id TEXT NOT NULL, + timestamp INTEGER NOT NULL, + event_name TEXT NOT NULL, + type TEXT NOT NULL, + topic TEXT, + trace TEXT AS List +); \ No newline at end of file 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 new file mode 100644 index 0000000000..216ba0860c --- /dev/null +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/EventsRepositoryTest.kt @@ -0,0 +1,100 @@ +package com.walletconnect.android.internal + +import android.database.sqlite.SQLiteException +import com.walletconnect.android.internal.common.model.TelemetryEnabled +import com.walletconnect.android.internal.common.storage.events.EventsRepository +import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.android.sdk.storage.data.dao.EventQueries +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.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertFailsWith + +class EventsRepositoryTest { + private val eventQueries: EventQueries = mockk() + private val telemetryEnabled: TelemetryEnabled = TelemetryEnabled(true) + private val bundleId: String = "testBundleId" + private val testDispatcher = StandardTestDispatcher() + private lateinit var repository: EventsRepository + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + repository = EventsRepository(eventQueries, bundleId, telemetryEnabled, testDispatcher) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @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()) } just Runs + + repository.insertOrAbort(props) + + verify { + eventQueries.insertOrAbort( + any(), bundleId, any(), "testEvent", "testType", null, null + ) + } + } + + @Test + fun `insertOrAbort should not insert event when telemetry is disabled`() = runTest(testDispatcher) { + repository = EventsRepository(eventQueries, bundleId, TelemetryEnabled(false), testDispatcher) + val props = Props(event = "testEvent", type = "testType") + + repository.insertOrAbort(props) + + verify(exactly = 0) { + eventQueries.insertOrAbort(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()) } throws SQLiteException() + + assertFailsWith { + repository.insertOrAbort(props) + } + } + + @Test + fun `deleteAll should delete all events`() = runTest(testDispatcher) { + coEvery { eventQueries.deleteAll() } just Runs + + repository.deleteAll() + + coVerify { eventQueries.deleteAll() } + } + + @Test + fun `deleteByIds should delete events by ids`() = runTest(testDispatcher) { + val eventIds = listOf(1L, 2L, 3L) + coEvery { eventQueries.deleteByIds(eventIds) } just Runs + + repository.deleteByIds(eventIds) + + coVerify { eventQueries.deleteByIds(eventIds) } + } +} \ No newline at end of file diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/InsertEventUseCaseTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/InsertEventUseCaseTest.kt new file mode 100644 index 0000000000..91aa7ae5ba --- /dev/null +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/InsertEventUseCaseTest.kt @@ -0,0 +1,68 @@ +package com.walletconnect.android.internal + +import com.walletconnect.android.internal.common.storage.events.EventsRepository +import com.walletconnect.android.pulse.domain.InsertEventUseCase +import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.foundation.util.Logger +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +class InsertEventUseCaseTest { + + private val eventsRepository: EventsRepository = mockk() + private val logger: Logger = mockk() + private val useCase = InsertEventUseCase(eventsRepository, logger) + private val testDispatcher = StandardTestDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `invoke should call insertOrAbort on eventsRepository with given props`() = runTest(testDispatcher) { + val props = Props(type = "test") + + coEvery { eventsRepository.insertOrAbort(props) } just Runs + + useCase(props) + + coVerify { eventsRepository.insertOrAbort(props) } + confirmVerified(eventsRepository) + } + @Test + fun `invoke should log error when eventsRepository throws exception`() = runTest(testDispatcher) { + val props = Props(type = "test") + val exception = Exception("Test exception") + + coEvery { eventsRepository.insertOrAbort(props) } throws exception + every { logger.error("Inserting event test error: java.lang.Exception: Test exception") } just Runs + + useCase(props) + + verify { logger.error("Inserting event test error: java.lang.Exception: Test exception") } + confirmVerified(logger) + } +} \ No newline at end of file diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/SendBatchEventUseCaseTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/SendBatchEventUseCaseTest.kt new file mode 100644 index 0000000000..ea9c3ad419 --- /dev/null +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/SendBatchEventUseCaseTest.kt @@ -0,0 +1,133 @@ +package com.walletconnect.android.internal + +import com.walletconnect.android.internal.common.model.TelemetryEnabled +import com.walletconnect.android.internal.common.storage.events.EventsRepository +import com.walletconnect.android.pulse.data.PulseService +import com.walletconnect.android.pulse.domain.SendBatchEventUseCase +import com.walletconnect.android.pulse.model.Event +import com.walletconnect.android.pulse.model.SDKType +import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.foundation.util.Logger +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.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Before +import org.junit.Test +import retrofit2.Response + +class SendBatchEventUseCaseTest { + private val pulseService: PulseService = mockk() + private val eventsRepository: EventsRepository = mockk() + private val telemetryEnabled: TelemetryEnabled = TelemetryEnabled(true) + private val logger: Logger = mockk() + private lateinit var useCase: SendBatchEventUseCase + private val testDispatcher = StandardTestDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + useCase = SendBatchEventUseCase(pulseService, eventsRepository, telemetryEnabled, logger) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `invoke should send batch events when telemetry is enabled and response is successful`() = runTest(testDispatcher) { + val props = Props(type = "testEvent") + val bundleId = "testBundleId" + val events = listOf(Event(eventId = 1, props = props, bundleId = bundleId), Event(eventId = 2, props = props, bundleId = bundleId)) + coEvery { eventsRepository.getAllWithLimitAndOffset(any(), any()) } returns events andThen listOf() + coEvery { pulseService.sendEventBatch(any(), any()) } returns Response.success(Unit) + coEvery { eventsRepository.deleteByIds(any()) } just Runs + every { logger.log("Sending batch events: ${events.size}") } just Runs + + useCase.invoke() + advanceUntilIdle() + + coVerify(exactly = 1) { pulseService.sendEventBatch(body = events, sdkType = SDKType.EVENTS.type) } + coVerify { eventsRepository.deleteByIds(events.map { it.eventId }) } + verify { logger.log("Sending batch events: ${events.size}") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `invoke should log error and stop processing when response is unsuccessful`() = runTest(testDispatcher) { + val props = Props(type = "testEvent") + val bundleId = "testBundleId" + val events = listOf(Event(eventId = 1, props = props, bundleId = bundleId), Event(eventId = 2, props = props, bundleId = bundleId)) + coEvery { eventsRepository.getAllWithLimitAndOffset(any(), any()) } returns events + coEvery { pulseService.sendEventBatch(any(), any()) } returns Response.error(400, "Error".toResponseBody()) + every { logger.log("Sending batch events: ${events.size}") } just Runs + every { logger.log("Failed to send events: ${events.size}") } just Runs + + useCase.invoke() + advanceUntilIdle() + + coVerify(exactly = 1) { pulseService.sendEventBatch(body = events, sdkType = SDKType.EVENTS.type) } + verify { logger.log("Failed to send events: ${events.size}") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `invoke should log error and stop processing when exception occurs`() = runTest(testDispatcher) { + val props = Props(type = "testEvent") + val bundleId = "testBundleId" + val events = listOf(Event(eventId = 1, props = props, bundleId = bundleId), Event(eventId = 2, props = props, bundleId = bundleId)) + coEvery { eventsRepository.getAllWithLimitAndOffset(any(), any()) } returns events + coEvery { pulseService.sendEventBatch(any(), any()) } throws Exception("Test exception") + every { logger.log("Sending batch events: ${events.size}") } just Runs + every { logger.error("Error sending batch events: Test exception") } just Runs + + useCase.invoke() + advanceUntilIdle() + + coVerify(exactly = 1) { pulseService.sendEventBatch(body = events, sdkType = SDKType.EVENTS.type) } + verify { logger.error("Error sending batch events: Test exception") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `invoke should delete all events when telemetry is disabled`() = runTest(testDispatcher) { + useCase = SendBatchEventUseCase(pulseService, eventsRepository, TelemetryEnabled(false), logger) + coEvery { eventsRepository.deleteAll() } just Runs + + useCase.invoke() + advanceUntilIdle() + + coVerify(exactly = 1) { eventsRepository.deleteAll() } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `invoke should log error when deleting all events fails`() = runTest(testDispatcher) { + useCase = SendBatchEventUseCase(pulseService, eventsRepository, TelemetryEnabled(false), logger) + val exception = Exception("Test exception") + coEvery { eventsRepository.deleteAll() } throws exception + every { logger.error("Failed to delete events, error: java.lang.Exception: Test exception") } just Runs + + useCase.invoke() + advanceUntilIdle() + + coVerify(exactly = 1) { eventsRepository.deleteAll() } + verify { logger.error("Failed to delete events, error: $exception") } + } +} \ No newline at end of file diff --git a/core/android/src/test/kotlin/com/walletconnect/android/internal/SendEventUseCaseTest.kt b/core/android/src/test/kotlin/com/walletconnect/android/internal/SendEventUseCaseTest.kt new file mode 100644 index 0000000000..81406a1071 --- /dev/null +++ b/core/android/src/test/kotlin/com/walletconnect/android/internal/SendEventUseCaseTest.kt @@ -0,0 +1,142 @@ +package com.walletconnect.android.internal + +import com.walletconnect.android.internal.common.di.AndroidCommonDITags +import com.walletconnect.android.internal.common.wcKoinApp +import com.walletconnect.android.pulse.data.PulseService +import com.walletconnect.android.pulse.domain.SendEventUseCase +import com.walletconnect.android.pulse.model.Event +import com.walletconnect.android.pulse.model.SDKType +import com.walletconnect.android.pulse.model.properties.Props +import com.walletconnect.foundation.util.Logger +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.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.core.module.Module +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.koin.test.KoinTest +import retrofit2.Response + +class SendEventUseCaseTest : KoinTest { + private val pulseService: PulseService = mockk() + private val logger: Logger = mockk() + private val bundleId: String = "testBundleId" + private lateinit var useCase: SendEventUseCase + private val testDispatcher = StandardTestDispatcher() + + private val testModule: Module = module { + single(named(AndroidCommonDITags.ENABLE_WEB_3_MODAL_ANALYTICS)) { true } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + val app = startKoin { modules(testModule) } + useCase = SendEventUseCase(pulseService, logger, bundleId) + wcKoinApp = app + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + stopKoin() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `send should log and send event when analytics is enabled and response is successful`() = runTest(testDispatcher) { + val props = Props(type = "testEvent") + val sdkType = SDKType.WEB3MODAL + val event = Event(props = props, bundleId = bundleId) + + coEvery { pulseService.sendEvent(body = any(), sdkType = any()) } returns Response.success(Unit) + every { logger.log("Event sent successfully: ${event.props.type}") } just Runs + + useCase.send(props, sdkType, event.timestamp, event.eventId) + + advanceUntilIdle() + + coVerify { pulseService.sendEvent(body = event, sdkType = sdkType.type) } + verify { logger.log("Event sent successfully: ${event.props.type}") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `send should log error when analytics is enabled and response is unsuccessful`() = runTest(testDispatcher) { + val props = Props(type = "testEvent") + val sdkType = SDKType.WEB3MODAL + val event = Event(props = props, bundleId = bundleId) + + coEvery { pulseService.sendEvent(any(), any()) } returns Response.error(400, "ResponseBody".toResponseBody()) + every { logger.error("Failed to send event: ${event.props.type}") } just Runs + + useCase.send(props, sdkType, event.timestamp, event.eventId) + + advanceUntilIdle() + + coVerify { pulseService.sendEvent(body = event, sdkType = sdkType.type) } + verify { logger.error("Failed to send event: ${event.props.type}") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `send should log exception when analytics is enabled and an exception occurs`() = runTest(testDispatcher) { + val props = Props(type = "testEvent") + val sdkType = SDKType.WEB3MODAL + val event = Event(props = props, bundleId = bundleId) + val exception = Exception("Test exception") + + coEvery { pulseService.sendEvent(any(), any()) } throws exception + every { logger.error("Failed to send event: ${props.type}, error: $exception") } just Runs + + useCase.send(props, sdkType, event.timestamp, event.eventId) + + advanceUntilIdle() + + coVerify { pulseService.sendEvent(body = event, sdkType = sdkType.type) } + verify { logger.error("Failed to send event: ${props.type}, error: $exception") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `send should not send event when analytics is disabled`() = runTest(testDispatcher) { + stopKoin() + val module: Module = module { + single(named(AndroidCommonDITags.ENABLE_WEB_3_MODAL_ANALYTICS)) { false } + } + val app2 = startKoin { modules(module) } + useCase = SendEventUseCase(pulseService, logger, bundleId) + wcKoinApp = app2 + + val props = Props(type = "testEvent") + val sdkType = SDKType.WEB3MODAL + val event = Event(props = props, bundleId = bundleId) + + useCase.send(props, sdkType, event.timestamp, event.eventId) + + advanceUntilIdle() + + coVerify(exactly = 0) { pulseService.sendEvent(any(), any()) } + verify(exactly = 0) { logger.log("") } + verify(exactly = 0) { logger.error("") } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5df3fac223..2470d9859b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -114,6 +114,7 @@ okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" } koin-jvm = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } jUnit = { module = "junit:junit", version.ref = "jUnit" } jUnit-engine = { module = "org.junit.vintage:junit-vintage-engine", version = "5.10.0" } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/client/Web3Modal.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/client/Web3Modal.kt index e839a1fb22..18eaed717c 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/client/Web3Modal.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/client/Web3Modal.kt @@ -4,6 +4,8 @@ import androidx.activity.ComponentActivity import com.walletconnect.android.internal.common.di.AndroidCommonDITags import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.wcKoinApp +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.sign.client.Sign import com.walletconnect.sign.client.SignClient import com.walletconnect.sign.common.exceptions.SignClientAlreadyInitializedException @@ -117,13 +119,13 @@ object Web3Modal { web3ModalEngine.setup(init, onError) web3ModalEngine.setInternalDelegate(Web3ModalDelegate) wcKoinApp.modules( - module { single(named(AndroidCommonDITags.ENABLE_ANALYTICS)) { init.enableAnalytics ?: web3ModalEngine.fetchAnalyticsConfig() } } + module { single(named(AndroidCommonDITags.ENABLE_WEB_3_MODAL_ANALYTICS)) { init.enableAnalytics ?: web3ModalEngine.fetchAnalyticsConfig() } } ) } .onFailure { error -> return@onInitializedClient onError(Modal.Model.Error(error)) } .onSuccess { onSuccess() - web3ModalEngine.sendModalLoadedEvent() + web3ModalEngine.send(Props(event = EventType.TRACK, type = EventType.Track.MODAL_LOADED)) } } else { onError(Modal.Model.Error(Web3ModelClientAlreadyInitializedException())) @@ -134,7 +136,6 @@ object Web3Modal { this.chains = chains } - fun setSessionProperties(properties: Map) { sessionProperties = properties } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/EngineModule.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/EngineModule.kt index f45a0e6b29..ef25b9103d 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/EngineModule.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/EngineModule.kt @@ -21,13 +21,9 @@ internal fun engineModule() = module { getSelectedChainUseCase = get(), deleteSessionDataUseCase = get(), saveSessionUseCase = get(), - sendModalLoadedUseCase = get(), - sendDisconnectErrorUseCase = get(), - sendDisconnectSuccessUseCase = get(), - sendConnectErrorUseCase = get(), - sendConnectSuccessUseCase = get(), connectionEventRepository = get(), enableAnalyticsUseCase = get(), + sendEventUseCase = get(), logger = get(named(AndroidCommonDITags.LOGGER)), ) } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/Web3ModuleModule.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/Web3ModalModule.kt similarity index 100% rename from product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/Web3ModuleModule.kt rename to product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/di/Web3ModalModule.kt diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/engine/Web3ModalEngine.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/engine/Web3ModalEngine.kt index ee6fc04dc6..b4d9a45d10 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/engine/Web3ModalEngine.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/engine/Web3ModalEngine.kt @@ -8,11 +8,10 @@ import androidx.activity.result.contract.ActivityResultContracts import com.walletconnect.android.internal.common.modal.domain.usecase.EnableAnalyticsUseCaseInterface import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.wcKoinApp -import com.walletconnect.android.pulse.domain.SendConnectErrorUseCase -import com.walletconnect.android.pulse.domain.SendConnectSuccessUseCase -import com.walletconnect.android.pulse.domain.SendDisconnectErrorUseCase -import com.walletconnect.android.pulse.domain.SendDisconnectSuccessUseCase -import com.walletconnect.android.pulse.domain.SendModalLoadedUseCaseInterface +import com.walletconnect.android.pulse.domain.SendEventInterface +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.foundation.util.Logger import com.walletconnect.sign.client.Sign import com.walletconnect.sign.client.SignClient @@ -50,15 +49,11 @@ internal class Web3ModalEngine( private val getSelectedChainUseCase: GetSelectedChainUseCase, private val saveSessionUseCase: SaveSessionUseCase, private val deleteSessionDataUseCase: DeleteSessionDataUseCase, - private val sendModalLoadedUseCase: SendModalLoadedUseCaseInterface, - private val sendDisconnectSuccessUseCase: SendDisconnectSuccessUseCase, - private val sendDisconnectErrorUseCase: SendDisconnectErrorUseCase, - private val sendConnectErrorUseCase: SendConnectErrorUseCase, - private val sendConnectSuccessUseCase: SendConnectSuccessUseCase, + private val sendEventUseCase: SendEventInterface, private val connectionEventRepository: ConnectionEventRepository, private val enableAnalyticsUseCase: EnableAnalyticsUseCaseInterface, private val logger: Logger -) : SendModalLoadedUseCaseInterface by sendModalLoadedUseCase, +) : SendEventInterface by sendEventUseCase, EnableAnalyticsUseCaseInterface by enableAnalyticsUseCase { internal var excludedWalletsIds: MutableList = mutableListOf() internal var recommendedWalletsIds: MutableList = mutableListOf() @@ -193,11 +188,11 @@ internal class Web3ModalEngine( is Session.WalletConnect -> { SignClient.disconnect(Sign.Params.Disconnect(session.topic), onSuccess = { - sendDisconnectSuccessUseCase() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.DISCONNECT_SUCCESS)) onSuccess() }, onError = { - sendDisconnectErrorUseCase() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.DISCONNECT_ERROR)) onError(it.throwable) }) } @@ -228,7 +223,7 @@ internal class Web3ModalEngine( override fun onSessionApproved(approvedSession: Sign.Model.ApprovedSession) { try { val (name, method) = connectionEventRepository.getEvent() - sendConnectSuccessUseCase(name = name, method = method) + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CONNECT_SUCCESS, Properties(name = name, method = method))) connectionEventRepository.deleteEvent() } catch (e: Exception) { logger.error(e) @@ -243,7 +238,7 @@ internal class Web3ModalEngine( } catch (e: Exception) { logger.error(e) } - sendConnectErrorUseCase(message = rejectedSession.reason) + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CONNECT_ERROR, Properties(message = rejectedSession.reason))) delegate.onSessionRejected(rejectedSession.toModal()) } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/button/Web3ModalState.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/button/Web3ModalState.kt index f4c1c58c5d..ed5f71a36f 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/button/Web3ModalState.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/button/Web3ModalState.kt @@ -5,9 +5,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavController import com.walletconnect.android.internal.common.wcKoinApp -import com.walletconnect.android.pulse.domain.SendClickNetworksUseCase -import com.walletconnect.android.pulse.domain.SendModalCloseUseCase -import com.walletconnect.android.pulse.domain.SendModalOpenUseCase +import com.walletconnect.android.pulse.domain.SendEventInterface +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.foundation.util.Logger import com.walletconnect.web3.modal.client.Modal import com.walletconnect.web3.modal.client.Web3Modal @@ -50,9 +51,7 @@ class Web3ModalState( private val getSessionUseCase: GetSessionUseCase = wcKoinApp.koin.get() private val getEthBalanceUseCase: GetEthBalanceUseCase = wcKoinApp.koin.get() private val web3ModalEngine: Web3ModalEngine = wcKoinApp.koin.get() - private val sendModalOpenEvent: SendModalOpenUseCase = wcKoinApp.koin.get() - private val sendModalCloseEvent: SendModalCloseUseCase = wcKoinApp.koin.get() - private val sendClickNetworksEvent: SendClickNetworksUseCase = wcKoinApp.koin.get() + private val sendEventUseCase: SendEventInterface = wcKoinApp.koin.get() private val sessionTopicFlow = observeSessionTopicUseCase() val isOpen = ComponentDelegate.modalComponentEvent @@ -84,10 +83,10 @@ class Web3ModalState( private fun sendModalCloseOrOpenEvents(event: ComponentEvent) { when { - event.isOpen && isConnected.value -> sendModalOpenEvent(connected = true) - event.isOpen && !isConnected.value -> sendModalOpenEvent(connected = false) - !event.isOpen && isConnected.value -> sendModalCloseEvent(connected = true) - !event.isOpen && !isConnected.value -> sendModalCloseEvent(connected = false) + event.isOpen && isConnected.value -> sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.MODAL_OPEN, Properties(connected = true))) + event.isOpen && !isConnected.value -> sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.MODAL_OPEN, Properties(connected = false))) + !event.isOpen && isConnected.value -> sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.MODAL_CLOSE, Properties(connected = true))) + !event.isOpen && !isConnected.value -> sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.MODAL_CLOSE, Properties(connected = false))) } } @@ -116,7 +115,7 @@ class Web3ModalState( internal fun openWeb3Modal(shouldOpenChooseNetwork: Boolean = false, isActiveNetwork: Boolean = false) { if (shouldOpenChooseNetwork && isActiveNetwork) { - sendClickNetworksEvent() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CLICK_NETWORKS)) } navController.openWeb3Modal( diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/internal/root/Web3ModalRootState.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/internal/root/Web3ModalRootState.kt index c48f6a3d68..e192002640 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/internal/root/Web3ModalRootState.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/components/internal/root/Web3ModalRootState.kt @@ -6,7 +6,9 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavDestination import com.walletconnect.android.internal.common.wcKoinApp -import com.walletconnect.android.pulse.domain.SendClickWalletHelpUseCase +import com.walletconnect.android.pulse.domain.SendEventInterface +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.web3.modal.ui.navigation.Route import com.walletconnect.web3.modal.ui.navigation.getTitleArg import kotlinx.coroutines.CoroutineScope @@ -27,7 +29,7 @@ internal class Web3ModalRootState( private val coroutineScope: CoroutineScope, private val navController: NavController ) { - private val sendClickWalletHelpUseCase: SendClickWalletHelpUseCase = wcKoinApp.koin.get() + private val sendEventUseCase: SendEventInterface = wcKoinApp.koin.get() val currentDestinationFlow: Flow get() = navController.currentBackStackEntryFlow @@ -41,7 +43,7 @@ internal class Web3ModalRootState( get() = navController.currentDestination?.route fun navigateToHelp() { - sendClickWalletHelpUseCase() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CLICK_WALLET_HELP,)) navController.navigate(Route.WHAT_IS_WALLET.path) } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/account/AccountViewModel.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/account/AccountViewModel.kt index 1da05b37f5..f6ba2e23d0 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/account/AccountViewModel.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/account/AccountViewModel.kt @@ -3,8 +3,10 @@ package com.walletconnect.web3.modal.ui.routes.account import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.walletconnect.android.internal.common.wcKoinApp -import com.walletconnect.android.pulse.domain.SendClickNetworkHelpUseCase -import com.walletconnect.android.pulse.domain.SendSwitchNetworkUseCase +import com.walletconnect.android.pulse.domain.SendEventInterface +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.foundation.util.Logger import com.walletconnect.modal.ui.model.UiState import com.walletconnect.web3.modal.client.Modal @@ -47,9 +49,7 @@ internal class AccountViewModel : ViewModel(), Navigator by NavigatorImpl() { private val getIdentityUseCase: GetIdentityUseCase = wcKoinApp.koin.get() private val getEthBalanceUseCase: GetEthBalanceUseCase = wcKoinApp.koin.get() private val web3ModalEngine: Web3ModalEngine = wcKoinApp.koin.get() - private val sendClickNetworkHelpUseCase: SendClickNetworkHelpUseCase = wcKoinApp.koin.get() - private val sendSwitchNetworkUseCase: SendSwitchNetworkUseCase = wcKoinApp.koin.get() - + private val sendEventUseCase: SendEventInterface = wcKoinApp.koin.get() private val activeSessionFlow = observeSessionUseCase() private val accountDataFlow = activeSessionFlow @@ -101,7 +101,7 @@ internal class AccountViewModel : ViewModel(), Navigator by NavigatorImpl() { fun changeActiveChain(chain: Modal.Model.Chain) = viewModelScope.launch { if (accountData.chains.contains(chain)) { - sendSwitchNetworkUseCase(network = chain.id) + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.SWITCH_NETWORK, Properties(network = chain.id))) saveChainSelectionUseCase(chain.id) popBackStack() } else { @@ -111,7 +111,7 @@ internal class AccountViewModel : ViewModel(), Navigator by NavigatorImpl() { suspend fun updatedSessionAfterChainSwitch(updatedSession: Session) { if (updatedSession.getChains().any { it.id == updatedSession.chain }) { - sendSwitchNetworkUseCase(network = updatedSession.chain) + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.SWITCH_NETWORK, Properties(network = updatedSession.chain))) saveSessionUseCase(updatedSession) popBackStack(path = Route.CHANGE_NETWORK.path, inclusive = true) } @@ -177,7 +177,7 @@ internal class AccountViewModel : ViewModel(), Navigator by NavigatorImpl() { fun getSelectedChainOrFirst() = web3ModalEngine.getSelectedChainOrFirst() fun navigateToHelp() { - sendClickNetworkHelpUseCase() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CLICK_NETWORK_HELP)) navigateTo(Route.WHAT_IS_WALLET.path) } } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/ConnectViewModel.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/ConnectViewModel.kt index bd86ca76f4..3422d673d6 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/ConnectViewModel.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/ConnectViewModel.kt @@ -4,11 +4,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.walletconnect.android.internal.common.modal.data.model.Wallet import com.walletconnect.android.internal.common.wcKoinApp -import com.walletconnect.android.pulse.domain.SendClickAllWalletsUseCase -import com.walletconnect.android.pulse.domain.SendClickNetworkHelpUseCase -import com.walletconnect.android.pulse.domain.SendConnectErrorUseCase -import com.walletconnect.android.pulse.domain.SendSelectWalletUseCase +import com.walletconnect.android.pulse.domain.SendEventInterface import com.walletconnect.android.pulse.model.ConnectionMethod +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.foundation.util.Logger import com.walletconnect.modal.ui.model.LoadingState import com.walletconnect.modal.ui.model.UiState @@ -35,20 +35,14 @@ internal class ConnectViewModel : ViewModel(), Navigator by NavigatorImpl(), Par private val saveRecentWalletUseCase: SaveRecentWalletUseCase = wcKoinApp.koin.get() private val saveChainSelectionUseCase: SaveChainSelectionUseCase = wcKoinApp.koin.get() private val observeSelectedChainUseCase: ObserveSelectedChainUseCase = wcKoinApp.koin.get() - private val sendClickAllWalletsEvent: SendClickAllWalletsUseCase = wcKoinApp.koin.get() private val web3ModalEngine: Web3ModalEngine = wcKoinApp.koin.get() - private val sendClickNetworkHelpUseCase: SendClickNetworkHelpUseCase = wcKoinApp.koin.get() - private val sendSelectWalletEvent: SendSelectWalletUseCase = wcKoinApp.koin.get() - private val sendConnectErrorUseCase: SendConnectErrorUseCase = wcKoinApp.koin.get() - + private val sendEventUseCase: SendEventInterface = wcKoinApp.koin.get() private var sessionParams = getSessionParamsSelectedChain(Web3Modal.selectedChain?.id) - val selectedChain = observeSelectedChainUseCase().map { savedChainId -> Web3Modal.chains.find { it.id == savedChainId } ?: web3ModalEngine.getSelectedChainOrFirst() } val walletsState: StateFlow = walletsDataStore.searchWalletsState.stateIn(viewModelScope, SharingStarted.Lazily, WalletsData.empty()) - val uiState: StateFlow>> = walletsDataStore.walletState.map { pagingData -> when { pagingData.error != null -> UiState.Error(pagingData.error) @@ -69,17 +63,17 @@ internal class ConnectViewModel : ViewModel(), Navigator by NavigatorImpl(), Par } fun navigateToHelp() { - sendClickNetworkHelpUseCase() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CLICK_NETWORK_HELP)) navigateTo(Route.WHAT_IS_WALLET.path) } fun navigateToScanQRCode() { - sendSelectWalletEvent(name = "WalletConnect", platform = ConnectionMethod.QR_CODE) + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.SELECT_WALLET, Properties(name = "WalletConnect", platform = ConnectionMethod.QR_CODE))) connectWalletConnect(name = "WalletConnect", method = ConnectionMethod.QR_CODE) { navigateTo(Route.QR_CODE.path) } } fun navigateToRedirectRoute(wallet: Wallet) { - sendSelectWalletEvent(name = wallet.name, platform = wallet.toConnectionType()) + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.SELECT_WALLET, Properties(name = wallet.name, platform = wallet.toConnectionType()))) saveRecentWalletUseCase(wallet.id) walletsDataStore.updateRecentWallet(wallet.id) navigateTo(wallet.toRedirectPath()) @@ -92,7 +86,7 @@ internal class ConnectViewModel : ViewModel(), Navigator by NavigatorImpl(), Par } fun navigateToAllWallets() { - sendClickAllWalletsEvent() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CLICK_ALL_WALLETS)) clearSearch() navigateTo(Route.ALL_WALLETS.path) } @@ -103,7 +97,7 @@ internal class ConnectViewModel : ViewModel(), Navigator by NavigatorImpl(), Par sessionParams = sessionParams, onSuccess = onSuccess, onError = { - sendConnectErrorUseCase(message = it.message ?: "Relay error while connecting") + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CONNECT_ERROR, Properties(message = it.message ?: "Relay error while connecting"))) showError(it.localizedMessage) logger.error(it) } diff --git a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/what_is_wallet/WhatIsWallet.kt b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/what_is_wallet/WhatIsWallet.kt index e7b72ec596..0ae450076f 100644 --- a/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/what_is_wallet/WhatIsWallet.kt +++ b/product/web3modal/src/main/kotlin/com/walletconnect/web3/modal/ui/routes/connect/what_is_wallet/WhatIsWallet.kt @@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.walletconnect.android.internal.common.wcKoinApp -import com.walletconnect.android.pulse.domain.SendClickGetWalletUseCase +import com.walletconnect.android.pulse.domain.SendEventInterface +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.web3.modal.R import com.walletconnect.web3.modal.ui.components.internal.commons.HelpSection import com.walletconnect.web3.modal.ui.components.internal.commons.VerticalSpacer @@ -28,9 +30,9 @@ import com.walletconnect.web3.modal.ui.theme.Web3ModalTheme internal fun WhatIsWallet( navController: NavController ) { - val sendClickGetWalletEvent: SendClickGetWalletUseCase = wcKoinApp.koin.get() + val sendEventUseCase: SendEventInterface = wcKoinApp.koin.get() WhatIsWallet { - sendClickGetWalletEvent() + sendEventUseCase.send(Props(EventType.TRACK, EventType.Track.CLICK_GET_WALLET)) navController.navigate(Route.GET_A_WALLET.path) } } diff --git a/protocol/auth/src/main/kotlin/com/walletconnect/auth/engine/domain/AuthEngine.kt b/protocol/auth/src/main/kotlin/com/walletconnect/auth/engine/domain/AuthEngine.kt index 9b987206d4..f4f16325a8 100644 --- a/protocol/auth/src/main/kotlin/com/walletconnect/auth/engine/domain/AuthEngine.kt +++ b/protocol/auth/src/main/kotlin/com/walletconnect/auth/engine/domain/AuthEngine.kt @@ -119,8 +119,8 @@ internal class AuthEngine( } private fun emitReceivedAuthRequest() { - pairingHandler.activePairingFlow - .onEach { pairingTopic -> + pairingHandler.inactivePairingFlow + .onEach { (pairingTopic, trace) -> try { val request = getPendingJsonRpcHistoryEntryByTopicUseCase(pairingTopic) val context = verifyContextStorageRepository.get(request.id) ?: VerifyContext(request.id, String.Empty, Validation.UNKNOWN, String.Empty, null) diff --git a/protocol/notify/src/androidTest/kotlin/com/walletconnect/notify/di/OverrideModule.kt b/protocol/notify/src/androidTest/kotlin/com/walletconnect/notify/di/OverrideModule.kt index e6b004f393..1aec2d573a 100644 --- a/protocol/notify/src/androidTest/kotlin/com/walletconnect/notify/di/OverrideModule.kt +++ b/protocol/notify/src/androidTest/kotlin/com/walletconnect/notify/di/OverrideModule.kt @@ -31,7 +31,7 @@ internal fun overrideModule( single { relay } includes( - coreStorageModule(storagePrefix), + coreStorageModule(storagePrefix, bundleId), corePairingModule(pairing, pairingController), coreCryptoModule(sharedPrefsFile, keyStoreAlias), coreJsonRpcModule(), diff --git a/protocol/sign/build.gradle.kts b/protocol/sign/build.gradle.kts index f029b17a8d..156e192184 100644 --- a/protocol/sign/build.gradle.kts +++ b/protocol/sign/build.gradle.kts @@ -105,6 +105,7 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.bundles.scarlet.test) testImplementation(libs.bundles.sqlDelight.test) + testImplementation(libs.koin.test) androidTestUtil(libs.androidx.testOrchestrator) androidTestImplementation(libs.bundles.androidxAndroidTest) diff --git a/protocol/sign/src/androidTest/kotlin/com/walletconnect/sign/di/OverrideModule.kt b/protocol/sign/src/androidTest/kotlin/com/walletconnect/sign/di/OverrideModule.kt index 432fb98448..c413354405 100644 --- a/protocol/sign/src/androidTest/kotlin/com/walletconnect/sign/di/OverrideModule.kt +++ b/protocol/sign/src/androidTest/kotlin/com/walletconnect/sign/di/OverrideModule.kt @@ -31,7 +31,7 @@ internal fun overrideModule( single { relay } includes( - coreStorageModule(storagePrefix), + coreStorageModule(storagePrefix, bundleId), corePairingModule(pairing, pairingController), coreCryptoModule(sharedPrefsFile, keyStoreAlias), coreAndroidNetworkModule(relayUrl, connectionType, "test_version", bundleId = bundleId), 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 8579d154ab..679c943927 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 @@ -91,6 +91,7 @@ internal fun callsModule() = module { sessionStorageRepository = get(), verifyContextStorageRepository = get(), pairingController = get(), + insertEventUseCase = get(), logger = get(named(AndroidCommonDITags.LOGGER)) ) } @@ -106,7 +107,8 @@ internal fun callsModule() = module { getPendingSessionAuthenticateRequest = get(), selfAppMetaData = get(), sessionStorageRepository = get(), - metadataStorageRepository = get() + metadataStorageRepository = get(), + insertEventUseCase = get() ) } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/EngineModule.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/EngineModule.kt index babd8d3770..c0751c4add 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/EngineModule.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/EngineModule.kt @@ -84,6 +84,7 @@ internal fun engineModule() = module { formatAuthenticateMessageUseCase = get(), deleteRequestByIdUseCase = get(), getPendingAuthenticateRequestUseCase = get(), + insertEventUseCase = get(), logger = get(named(AndroidCommonDITags.LOGGER)) ) } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/RequestsModule.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/RequestsModule.kt index e3a9aa51f1..e16d97a42c 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/RequestsModule.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/di/RequestsModule.kt @@ -22,11 +22,20 @@ internal fun requestsModule() = module { jsonRpcInteractor = get(), proposalStorageRepository = get(), resolveAttestationIdUseCase = get(), + insertEventUseCase = get(), logger = get(named(AndroidCommonDITags.LOGGER)) ) } - single { OnSessionAuthenticateUseCase(jsonRpcInteractor = get(), resolveAttestationIdUseCase = get(), logger = get(), pairingController = get()) } + single { + OnSessionAuthenticateUseCase( + jsonRpcInteractor = get(), + resolveAttestationIdUseCase = get(), + logger = get(), + pairingController = get(), + insertEventUseCase = get() + ) + } single { OnSessionSettleUseCase( 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 41a53f2237..2ab2869c85 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 @@ -13,6 +13,11 @@ import com.walletconnect.android.internal.common.storage.metadata.MetadataStorag import com.walletconnect.android.internal.common.storage.verify.VerifyContextStorageRepository import com.walletconnect.android.internal.utils.CoreValidator.isExpired import com.walletconnect.android.pairing.handler.PairingControllerInterface +import com.walletconnect.android.pulse.domain.InsertEventUseCase +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.push.notifications.DecryptMessageUseCaseInterface import com.walletconnect.android.relay.WSSConnectionState import com.walletconnect.android.verify.data.model.VerifyContext @@ -135,6 +140,7 @@ internal class SignEngine( private val onSessionSettleResponseUseCase: OnSessionSettleResponseUseCase, private val onSessionUpdateResponseUseCase: OnSessionUpdateResponseUseCase, private val onSessionRequestResponseUseCase: OnSessionRequestResponseUseCase, + private val insertEventUseCase: InsertEventUseCase, private val logger: Logger ) : ProposeSessionUseCaseInterface by proposeSessionUseCase, SessionAuthenticateUseCaseInterface by authenticateSessionUseCase, @@ -398,8 +404,8 @@ internal class SignEngine( } private fun emitReceivedPendingRequestsWhilePairingOnTheSameURL() { - pairingController.activePairingFlow - .onEach { pairingTopic -> + pairingController.inactivePairingFlow + .onEach { (pairingTopic, trace) -> try { val pendingAuthenticateRequests = getPendingAuthenticateRequestUseCase.getPendingAuthenticateRequests().filter { request -> request.topic == pairingTopic } if (pendingAuthenticateRequests.isNotEmpty()) { @@ -419,12 +425,14 @@ internal class SignEngine( } else { val proposal = proposalStorageRepository.getProposalByTopic(pairingTopic.value) if (proposal.expiry?.isExpired() == true) { + insertEventUseCase(Props(type = EventType.Error.PROPOSAL_EXPIRED, properties = Properties(trace = trace, topic = pairingTopic.value))) proposalStorageRepository.deleteProposal(proposal.proposerPublicKey) scope.launch { _engineEvent.emit(proposal.toExpiredProposal()) } } else { val context = verifyContextStorageRepository.get(proposal.requestId) ?: VerifyContext(proposal.requestId, String.Empty, Validation.UNKNOWN, String.Empty, null) val sessionProposalEvent = EngineDO.SessionProposalEvent(proposal = proposal.toEngineDO(), context = context.toEngineDO()) logger.log("Emitting session proposal from active pairing: $sessionProposalEvent") + trace.add(Trace.Pairing.EMIT_SESSION_PROPOSAL) scope.launch { _engineEvent.emit(sessionProposalEvent) } } } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCase.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCase.kt index 6a18c415cf..901575e6ec 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCase.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionAuthenticateUseCase.kt @@ -3,7 +3,8 @@ package com.walletconnect.sign.engine.use_case.calls import com.walletconnect.android.Core import com.walletconnect.android.internal.common.JsonRpcResponse import com.walletconnect.android.internal.common.crypto.kmr.KeyManagementRepository -import com.walletconnect.android.internal.common.exception.Invalid +import com.walletconnect.android.internal.common.exception.NoInternetConnectionException +import com.walletconnect.android.internal.common.exception.NoRelayConnectionException import com.walletconnect.android.internal.common.exception.RequestExpiredException import com.walletconnect.android.internal.common.model.AppMetaData import com.walletconnect.android.internal.common.model.AppMetaDataType @@ -14,9 +15,7 @@ import com.walletconnect.android.internal.common.model.Participant import com.walletconnect.android.internal.common.model.Participants import com.walletconnect.android.internal.common.model.SymmetricKey import com.walletconnect.android.internal.common.model.Tags -import com.walletconnect.android.internal.common.model.WCRequest import com.walletconnect.android.internal.common.model.params.CoreSignParams -import com.walletconnect.android.internal.common.model.type.ClientParams import com.walletconnect.android.internal.common.model.type.JsonRpcInteractorInterface import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.signing.cacao.Cacao @@ -28,8 +27,12 @@ import com.walletconnect.android.internal.common.storage.verify.VerifyContextSto import com.walletconnect.android.internal.utils.CoreValidator import com.walletconnect.android.internal.utils.CoreValidator.isExpired import com.walletconnect.android.internal.utils.dayInSeconds -import com.walletconnect.android.internal.utils.fiveMinutesInSeconds import com.walletconnect.android.pairing.handler.PairingControllerInterface +import com.walletconnect.android.pulse.domain.InsertEventUseCase +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.foundation.common.model.PublicKey import com.walletconnect.foundation.common.model.Topic import com.walletconnect.foundation.common.model.Ttl @@ -39,7 +42,6 @@ import com.walletconnect.sign.common.model.vo.clientsync.session.params.SignPara import com.walletconnect.sign.common.model.vo.sequence.SessionVO import com.walletconnect.sign.common.validator.SignValidator import com.walletconnect.sign.json_rpc.domain.GetPendingSessionAuthenticateRequest -import com.walletconnect.sign.json_rpc.model.JsonRpcMethod import com.walletconnect.sign.storage.sequence.SessionStorageRepository import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope @@ -54,45 +56,56 @@ internal class ApproveSessionAuthenticateUseCase( private val pairingController: PairingControllerInterface, private val metadataStorageRepository: MetadataStorageRepositoryInterface, private val selfAppMetaData: AppMetaData, - private val sessionStorageRepository: SessionStorageRepository + private val sessionStorageRepository: SessionStorageRepository, + private val insertEventUseCase: InsertEventUseCase, ) : ApproveSessionAuthenticateUseCaseInterface { override suspend fun approveSessionAuthenticate(id: Long, cacaos: List, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) = supervisorScope { + val trace: MutableList = mutableListOf() + trace.add(Trace.SessionAuthenticate.SESSION_AUTHENTICATE_APPROVE_STARTED).also { logger.log(Trace.SessionAuthenticate.SESSION_AUTHENTICATE_APPROVE_STARTED) } try { val jsonRpcHistoryEntry = getPendingSessionAuthenticateRequest(id) - if (jsonRpcHistoryEntry == null) { - logger.error(MissingSessionAuthenticateRequest().message) + insertEventUseCase(Props(type = EventType.Error.MISSING_SESSION_AUTH_REQUEST, properties = Properties(trace = trace))) + .also { logger.error(MissingSessionAuthenticateRequest().message) } onFailure(MissingSessionAuthenticateRequest()) return@supervisorScope } jsonRpcHistoryEntry.expiry?.let { if (it.isExpired()) { - val irnParams = IrnParams(Tags.SESSION_REQUEST_RESPONSE, Ttl(fiveMinutesInSeconds)) - val request = WCRequest(jsonRpcHistoryEntry.topic, jsonRpcHistoryEntry.id, JsonRpcMethod.WC_SESSION_AUTHENTICATE, object : ClientParams {}) - jsonRpcInteractor.respondWithError(request, Invalid.RequestExpired, irnParams) - logger.error("Session Authenticate Request Expired: ${jsonRpcHistoryEntry.topic}, id: ${jsonRpcHistoryEntry.id}") + insertEventUseCase(Props(type = EventType.Error.SESSION_AUTH_REQUEST_EXPIRED, properties = Properties(trace = trace))) + .also { logger.error("Session Authenticate Request Expired: ${jsonRpcHistoryEntry.topic}, id: ${jsonRpcHistoryEntry.id}") } throw RequestExpiredException("This request has expired, id: ${jsonRpcHistoryEntry.id}") } } - + trace.add(Trace.SessionAuthenticate.AUTHENTICATED_SESSION_NOT_EXPIRED) val sessionAuthenticateParams: SignParams.SessionAuthenticateParams = jsonRpcHistoryEntry.params val chains = cacaos.first().payload.resources.getChains().ifEmpty { sessionAuthenticateParams.authPayload.chains } - if (!chains.all { chain -> CoreValidator.isChainIdCAIP2Compliant(chain) }) throw Exception("Chains are not CAIP-2 compliant") - if (!chains.all { chain -> SignValidator.getNamespaceKeyFromChainId(chain) == "eip155" }) throw Exception("Only eip155 (EVM) is supported") - + if (!chains.all { chain -> CoreValidator.isChainIdCAIP2Compliant(chain) }) { + insertEventUseCase(Props(type = EventType.Error.CHAINS_CAIP2_COMPLIANT_FAILURE, properties = Properties(trace = trace))) + throw Exception("Chains are not CAIP-2 compliant") + } + trace.add(Trace.SessionAuthenticate.CHAINS_CAIP2_COMPLIANT) + if (!chains.all { chain -> SignValidator.getNamespaceKeyFromChainId(chain) == "eip155" }) { + insertEventUseCase(Props(type = EventType.Error.CHAINS_EVM_COMPLIANT_FAILURE, properties = Properties(trace = trace))) + throw Exception("Only eip155 (EVM) is supported") + } + trace.add(Trace.SessionAuthenticate.CHAINS_EVM_COMPLIANT) val receiverPublicKey = PublicKey(sessionAuthenticateParams.requester.publicKey) val receiverMetadata = sessionAuthenticateParams.requester.metadata val senderPublicKey: PublicKey = crypto.generateAndStoreX25519KeyPair() val symmetricKey: SymmetricKey = crypto.generateSymmetricKeyFromKeyAgreement(senderPublicKey, receiverPublicKey) val responseTopic: Topic = crypto.getTopicFromKey(receiverPublicKey) val sessionTopic = crypto.getTopicFromKey(symmetricKey) + trace.add(Trace.SessionAuthenticate.CREATE_AUTHENTICATED_SESSION_TOPIC) val irnParams = IrnParams(Tags.SESSION_AUTHENTICATE_RESPONSE_APPROVE, Ttl(dayInSeconds)) if (cacaos.find { cacao -> !cacaoVerifier.verify(cacao) } != null) { - logger.error("Invalid Cacao for Session Authenticate") + insertEventUseCase(Props(type = EventType.Error.INVALID_CACAO, properties = Properties(trace = trace, topic = sessionTopic.value))) + .also { logger.error("Invalid Cacao for Session Authenticate") } return@supervisorScope onFailure(Throwable("Signature verification failed Session Authenticate, please try again")) } + trace.add(Trace.SessionAuthenticate.CACAOS_VERIFIED) val addresses = cacaos.map { cacao -> Issuer(cacao.payload.iss).address }.distinct() val accounts = mutableListOf() @@ -118,24 +131,29 @@ internal class ApproveSessionAuthenticateUseCase( metadataStorageRepository.insertOrAbortMetadata(sessionTopic, selfAppMetaData, AppMetaDataType.SELF) metadataStorageRepository.insertOrAbortMetadata(sessionTopic, receiverMetadata, AppMetaDataType.PEER) sessionStorageRepository.insertSession(authenticatedSession, id) + trace.add(Trace.SessionAuthenticate.STORE_AUTHENTICATED_SESSION) } val responseParams = CoreSignParams.SessionAuthenticateApproveParams(responder = Participant(publicKey = senderPublicKey.keyAsHex, metadata = selfAppMetaData), cacaos = cacaos) val response: JsonRpcResponse = JsonRpcResponse.JsonRpcResult(id, result = responseParams) crypto.setKey(symmetricKey, sessionTopic.value) - logger.log("Subscribing Session Authenticate on topic: $responseTopic") - jsonRpcInteractor.subscribe(sessionTopic, onSuccess = { - logger.log("Subscribed Session Authenticate on topic: $responseTopic") - }, { error -> - logger.log("Subscribing Session Authenticate error on topic: $responseTopic, $error") - onFailure(error) - }) + trace.add(Trace.SessionAuthenticate.SUBSCRIBING_AUTHENTICATED_SESSION_TOPIC).also { logger.log("Subscribing Session Authenticate on topic: $responseTopic") } + jsonRpcInteractor.subscribe(sessionTopic, + onSuccess = { + trace.add(Trace.SessionAuthenticate.SUBSCRIBE_AUTHENTICATED_SESSION_TOPIC_SUCCESS).also { logger.log("Subscribed Session Authenticate on topic: $responseTopic") } + }, + onFailure = { error -> + scope.launch { + supervisorScope { insertEventUseCase(Props(type = EventType.Error.SUBSCRIBE_AUTH_SESSION_TOPIC_FAILURE, properties = Properties(trace = trace, topic = sessionTopic.value))) } + }.also { logger.log("Subscribing Session Authenticate error on topic: $responseTopic, $error") } + onFailure(error) + }) - logger.log("Sending Session Authenticate Approve on topic: $responseTopic") + trace.add(Trace.SessionAuthenticate.PUBLISHING_AUTHENTICATED_SESSION_APPROVE).also { logger.log("Sending Session Authenticate Approve on topic: $responseTopic") } jsonRpcInteractor.publishJsonRpcResponse(responseTopic, irnParams, response, envelopeType = EnvelopeType.ONE, participants = Participants(senderPublicKey, receiverPublicKey), onSuccess = { - logger.log("Session Authenticate Approve Responded on topic: $responseTopic") + trace.add(Trace.SessionAuthenticate.AUTHENTICATED_SESSION_APPROVE_PUBLISH_SUCCESS).also { logger.log("Session Authenticate Approve Responded on topic: $responseTopic") } onSuccess() scope.launch { supervisorScope { @@ -145,22 +163,25 @@ internal class ApproveSessionAuthenticateUseCase( } }, onFailure = { error -> - runCatching { - crypto.removeKeys(sessionTopic.value) - }.onFailure { logger.error(it) } + runCatching { crypto.removeKeys(sessionTopic.value) }.onFailure { logger.error(it) } sessionStorageRepository.deleteSession(sessionTopic) - logger.error("Error Responding Session Authenticate on topic: $responseTopic, error: $error") + scope.launch { + supervisorScope { + insertEventUseCase(Props(type = EventType.Error.AUTHENTICATED_SESSION_APPROVE_PUBLISH_FAILURE, properties = Properties(trace = trace, topic = responseTopic.value))) + } + }.also { logger.error("Error Responding Session Authenticate on topic: $responseTopic, error: $error") } onFailure(error) } ) } catch (e: Exception) { logger.error("Error Responding Session Authenticate, error: $e") + if (e is NoRelayConnectionException) insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION, properties = Properties(trace = trace))) + if (e is NoInternetConnectionException) insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION, properties = Properties(trace = trace))) onFailure(e) } } } internal interface ApproveSessionAuthenticateUseCaseInterface { - suspend fun approveSessionAuthenticate(id: Long, cacaos: List, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) } \ No newline at end of file diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionUseCase.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionUseCase.kt index 49fc7d6e98..30bf4eb6a5 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionUseCase.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/ApproveSessionUseCase.kt @@ -1,8 +1,9 @@ package com.walletconnect.sign.engine.use_case.calls -import android.database.sqlite.SQLiteException import com.walletconnect.android.Core import com.walletconnect.android.internal.common.crypto.kmr.KeyManagementRepository +import com.walletconnect.android.internal.common.exception.NoInternetConnectionException +import com.walletconnect.android.internal.common.exception.NoRelayConnectionException import com.walletconnect.android.internal.common.model.AppMetaData import com.walletconnect.android.internal.common.model.AppMetaDataType import com.walletconnect.android.internal.common.model.IrnParams @@ -15,6 +16,11 @@ import com.walletconnect.android.internal.utils.ACTIVE_SESSION 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.android.pulse.domain.InsertEventUseCase +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.foundation.common.model.PublicKey import com.walletconnect.foundation.common.model.Topic import com.walletconnect.foundation.common.model.Ttl @@ -45,6 +51,7 @@ internal class ApproveSessionUseCase( private val verifyContextStorageRepository: VerifyContextStorageRepository, private val selfAppMetaData: AppMetaData, private val pairingController: PairingControllerInterface, + private val insertEventUseCase: InsertEventUseCase, private val logger: Logger ) : ApproveSessionUseCaseInterface { @@ -54,21 +61,22 @@ internal class ApproveSessionUseCase( onSuccess: () -> Unit, onFailure: (Throwable) -> Unit ) = supervisorScope { + val trace: MutableList = mutableListOf() + trace.add(Trace.Session.SESSION_APPROVE_STARTED).also { logger.log(Trace.Session.SESSION_APPROVE_STARTED) } fun sessionSettle(requestId: Long, proposal: ProposalVO, sessionTopic: Topic, pairingTopic: Topic) { val selfPublicKey = crypto.getSelfPublicFromKeyAgreement(sessionTopic) val selfParticipant = SessionParticipant(selfPublicKey.keyAsHex, selfAppMetaData) val sessionExpiry = ACTIVE_SESSION val unacknowledgedSession = SessionVO.createUnacknowledgedSession(sessionTopic, proposal, selfParticipant, sessionExpiry, sessionNamespaces, pairingTopic.value) - try { sessionStorageRepository.insertSession(unacknowledgedSession, requestId) metadataStorageRepository.insertOrAbortMetadata(sessionTopic, selfAppMetaData, AppMetaDataType.SELF) metadataStorageRepository.insertOrAbortMetadata(sessionTopic, proposal.appMetaData, AppMetaDataType.PEER) + trace.add(Trace.Session.STORE_SESSION) val params = proposal.toSessionSettleParams(selfParticipant, sessionExpiry, sessionNamespaces) val sessionSettle = SignRpc.SessionSettle(params = params) val irnParams = IrnParams(Tags.SESSION_SETTLE, Ttl(fiveMinutesInSeconds)) - - logger.log("Sending session settle on topic: $sessionTopic") + trace.add(Trace.Session.PUBLISHING_SESSION_SETTLE).also { logger.log("Publishing session settle on topic: $sessionTopic") } jsonRpcInteractor.publishJsonRpcRequest( topic = sessionTopic, params = irnParams, sessionSettle, @@ -79,16 +87,32 @@ internal class ApproveSessionUseCase( pairingController.activate(Core.Params.Activate(pairingTopic.value)) proposalStorageRepository.deleteProposal(proposerPublicKey) verifyContextStorageRepository.delete(proposal.requestId) - logger.log("Session settle sent successfully on topic: $sessionTopic") + trace.add(Trace.Session.SESSION_SETTLE_PUBLISH_SUCCESS).also { logger.log("Session settle sent successfully on topic: $sessionTopic") } } } }, onFailure = { error -> - logger.error("Session settle failure on topic: $sessionTopic, error: $error") + scope.launch { + supervisorScope { + insertEventUseCase(Props(type = EventType.Error.SESSION_SETTLE_PUBLISH_FAILURE, properties = Properties(trace = trace, topic = sessionTopic.value))) + } + }.also { logger.error("Session settle failure on topic: $sessionTopic, error: $error") } onFailure(error) } ) - } catch (e: SQLiteException) { + } catch (e: Exception) { + if (e is NoRelayConnectionException) + scope.launch { + supervisorScope { + insertEventUseCase(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))) + } + } sessionStorageRepository.deleteSession(sessionTopic) logger.error("Session settle failure, error: $e") // todo: missing metadata deletion. Also check other try catches @@ -98,42 +122,60 @@ internal class ApproveSessionUseCase( val proposal = proposalStorageRepository.getProposalByKey(proposerPublicKey) val request = proposal.toSessionProposeRequest() - proposal.expiry?.let { - if (it.isExpired()) { - logger.error("Proposal expired on approve, topic: ${proposal.pairingTopic.value}, id: ${proposal.requestId}") - throw SessionProposalExpiredException("Session proposal expired") + val pairingTopic = proposal.pairingTopic.value + try { + proposal.expiry?.let { + if (it.isExpired()) { + insertEventUseCase(Props(type = EventType.Error.PROPOSAL_EXPIRED, properties = Properties(trace = trace, topic = pairingTopic))) + .also { logger.error("Proposal expired on approve, topic: $pairingTopic, id: ${proposal.requestId}") } + throw SessionProposalExpiredException("Session proposal expired") + } } - } + trace.add(Trace.Session.PROPOSAL_NOT_EXPIRED) + SignValidator.validateSessionNamespace(sessionNamespaces.toMapOfNamespacesVOSession(), proposal.requiredNamespaces) { error -> + insertEventUseCase(Props(type = EventType.Error.SESSION_APPROVE_NAMESPACE_VALIDATION_FAILURE, properties = Properties(trace = trace, topic = proposal.pairingTopic.value))) + .also { logger.log("Session approve failure - invalid namespaces, error: $error") } + throw InvalidNamespaceException(error.message) + } + trace.add(Trace.Session.SESSION_NAMESPACE_VALIDATION_SUCCESS) + val selfPublicKey: PublicKey = crypto.generateAndStoreX25519KeyPair() + val sessionTopic = crypto.generateTopicFromKeyAgreement(selfPublicKey, PublicKey(proposerPublicKey)) + trace.add(Trace.Session.CREATE_SESSION_TOPIC) + val approvalParams = proposal.toSessionApproveParams(selfPublicKey) + val irnParams = IrnParams(Tags.SESSION_PROPOSE_RESPONSE_APPROVE, Ttl(fiveMinutesInSeconds)) + trace.add(Trace.Session.SUBSCRIBING_SESSION_TOPIC).also { logger.log("Subscribing to session topic: $sessionTopic") } + jsonRpcInteractor.subscribe(sessionTopic, + onSuccess = { + trace.add(Trace.Session.SUBSCRIBE_SESSION_TOPIC_SUCCESS).also { logger.log("Successfully subscribed to session topic: $sessionTopic") } + }, + onFailure = { error -> + scope.launch { + supervisorScope { + insertEventUseCase(Props(type = EventType.Error.SESSION_SUBSCRIPTION_FAILURE, properties = Properties(trace = trace, topic = sessionTopic.value))) + } + }.also { logger.error("Subscribe to session topic failure: $error") } + onFailure(error) + }) + trace.add(Trace.Session.PUBLISHING_SESSION_APPROVE).also { logger.log("Publishing session approve on topic: $sessionTopic") } + jsonRpcInteractor.respondWithParams(request, approvalParams, irnParams, + onSuccess = { + trace.add(Trace.Session.SESSION_APPROVE_PUBLISH_SUCCESS).also { logger.log("Session approve sent successfully, topic: $sessionTopic") } + }, + onFailure = { error -> + scope.launch { + supervisorScope { + insertEventUseCase(Props(type = EventType.Error.SESSION_APPROVE_PUBLISH_FAILURE, properties = Properties(trace = trace, topic = sessionTopic.value))) + } + }.also { logger.error("Session approve failure, topic: $sessionTopic: $error") } + onFailure(error) + }) - SignValidator.validateSessionNamespace(sessionNamespaces.toMapOfNamespacesVOSession(), proposal.requiredNamespaces) { error -> - logger.log("Session approve failure - invalid namespaces, error: $error") - throw InvalidNamespaceException(error.message) + sessionSettle(request.id, proposal, sessionTopic, request.topic) + } catch (e: Exception) { + if (e is NoRelayConnectionException) insertEventUseCase(Props(type = EventType.Error.NO_WSS_CONNECTION, properties = Properties(trace = trace, topic = pairingTopic))) + if (e is NoInternetConnectionException) insertEventUseCase(Props(type = EventType.Error.NO_INTERNET_CONNECTION, properties = Properties(trace = trace, topic = pairingTopic))) + onFailure(e) } - - val selfPublicKey: PublicKey = crypto.generateAndStoreX25519KeyPair() - val sessionTopic = crypto.generateTopicFromKeyAgreement(selfPublicKey, PublicKey(proposerPublicKey)) - val approvalParams = proposal.toSessionApproveParams(selfPublicKey) - val irnParams = IrnParams(Tags.SESSION_PROPOSE_RESPONSE_APPROVE, Ttl(fiveMinutesInSeconds)) - logger.log("Subscribing to session topic: $sessionTopic") - jsonRpcInteractor.subscribe(sessionTopic, - onSuccess = { - logger.log("Successfully subscribed to session topic: $sessionTopic") - }, - onFailure = { error -> - logger.error("Subscribe to session topic failure: $error") - onFailure(error) - }) - logger.log("Sending session approve, topic: $sessionTopic") - jsonRpcInteractor.respondWithParams(request, approvalParams, irnParams, - onSuccess = { - logger.log("Session approve sent successfully, topic: $sessionTopic") - }, - onFailure = { error -> - logger.error("Session approve failure, topic: $sessionTopic: $error") - onFailure(error) - }) - - sessionSettle(request.id, proposal, sessionTopic, request.topic) } } diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCase.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCase.kt index 76707f47f0..0cc0a7d026 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCase.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCase.kt @@ -45,7 +45,7 @@ internal class RejectSessionAuthenticateUseCase( jsonRpcHistoryEntry.expiry?.let { if (it.isExpired()) { - val irnParams = IrnParams(Tags.SESSION_REQUEST_RESPONSE, Ttl(fiveMinutesInSeconds)) + val irnParams = IrnParams(Tags.SESSION_AUTHENTICATE_RESPONSE_REJECT, Ttl(fiveMinutesInSeconds)) val request = WCRequest(jsonRpcHistoryEntry.topic, jsonRpcHistoryEntry.id, JsonRpcMethod.WC_SESSION_AUTHENTICATE, object : ClientParams {}) jsonRpcInteractor.respondWithError(request, Invalid.RequestExpired, irnParams) logger.error("Session Authenticate Request Expired: ${jsonRpcHistoryEntry.topic}, id: ${jsonRpcHistoryEntry.id}") diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionAuthenticateUseCase.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionAuthenticateUseCase.kt index 480828e966..2acc518eec 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionAuthenticateUseCase.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionAuthenticateUseCase.kt @@ -14,6 +14,10 @@ import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.utils.CoreValidator.isExpired import com.walletconnect.android.internal.utils.dayInSeconds import com.walletconnect.android.pairing.handler.PairingControllerInterface +import com.walletconnect.android.pulse.domain.InsertEventUseCase +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.android.verify.domain.ResolveAttestationIdUseCase import com.walletconnect.foundation.common.model.Ttl import com.walletconnect.foundation.util.Logger @@ -30,6 +34,7 @@ internal class OnSessionAuthenticateUseCase( private val jsonRpcInteractor: JsonRpcInteractorInterface, private val resolveAttestationIdUseCase: ResolveAttestationIdUseCase, private val pairingController: PairingControllerInterface, + private val insertEventUseCase: InsertEventUseCase, private val logger: Logger ) { private val _events: MutableSharedFlow = MutableSharedFlow() @@ -41,6 +46,7 @@ internal class OnSessionAuthenticateUseCase( try { if (Expiry(authenticateSessionParams.expiryTimestamp).isExpired()) { logger.log("Received session authenticate - expiry error: ${request.topic}") + .also { insertEventUseCase(Props(type = EventType.Error.AUTHENTICATED_SESSION_EXPIRED, properties = Properties(topic = request.topic.value))) } jsonRpcInteractor.respondWithError(request, Invalid.RequestExpired, irnParams) _events.emit(SDKError(Throwable("Received session authenticate - expiry error: ${request.topic}"))) return@supervisorScope diff --git a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionProposalUseCase.kt b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionProposalUseCase.kt index fd39632cfa..0046d348ed 100644 --- a/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionProposalUseCase.kt +++ b/protocol/sign/src/main/kotlin/com/walletconnect/sign/engine/use_case/requests/OnSessionProposalUseCase.kt @@ -14,6 +14,10 @@ import com.walletconnect.android.internal.common.scope import com.walletconnect.android.internal.common.wcKoinApp import com.walletconnect.android.internal.utils.fiveMinutesInSeconds import com.walletconnect.android.pairing.handler.PairingControllerInterface +import com.walletconnect.android.pulse.domain.InsertEventUseCase +import com.walletconnect.android.pulse.model.EventType +import com.walletconnect.android.pulse.model.properties.Properties +import com.walletconnect.android.pulse.model.properties.Props import com.walletconnect.android.utils.toClient import com.walletconnect.android.verify.domain.ResolveAttestationIdUseCase import com.walletconnect.foundation.common.model.Ttl @@ -38,6 +42,7 @@ internal class OnSessionProposalUseCase( private val proposalStorageRepository: ProposalStorageRepository, private val resolveAttestationIdUseCase: ResolveAttestationIdUseCase, private val pairingController: PairingControllerInterface, + private val insertEventUseCase: InsertEventUseCase, private val logger: Logger ) { private val _events: MutableSharedFlow = MutableSharedFlow() @@ -54,12 +59,14 @@ internal class OnSessionProposalUseCase( logger.log("Session proposal received: ${request.topic}") SignValidator.validateProposalNamespaces(payloadParams.requiredNamespaces) { error -> logger.error("Session proposal received error: required namespace validation: ${error.message}") + insertEventUseCase(Props(type = EventType.Error.REQUIRED_NAMESPACE_VALIDATION_FAILURE, properties = Properties(topic = request.topic.value))) jsonRpcInteractor.respondWithError(request, error.toPeerError(), irnParams) return@supervisorScope } SignValidator.validateProposalNamespaces(payloadParams.optionalNamespaces ?: emptyMap()) { error -> logger.error("Session proposal received error: optional namespace validation: ${error.message}") + insertEventUseCase(Props(type = EventType.Error.OPTIONAL_NAMESPACE_VALIDATION_FAILURE, properties = Properties(topic = request.topic.value))) jsonRpcInteractor.respondWithError(request, error.toPeerError(), irnParams) return@supervisorScope } @@ -67,6 +74,7 @@ internal class OnSessionProposalUseCase( payloadParams.properties?.let { SignValidator.validateProperties(payloadParams.properties) { error -> logger.error("Session proposal received error: session properties validation: ${error.message}") + insertEventUseCase(Props(type = EventType.Error.SESSION_PROPERTIES_VALIDATION_FAILURE, properties = Properties(topic = request.topic.value))) jsonRpcInteractor.respondWithError(request, error.toPeerError(), irnParams) return@supervisorScope } diff --git a/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ExtendSessionUseCaseTest.kt b/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ExtendSessionUseCaseTest.kt index ba3eeca4dd..0528ce0c71 100644 --- a/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ExtendSessionUseCaseTest.kt +++ b/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/ExtendSessionUseCaseTest.kt @@ -7,7 +7,6 @@ import com.walletconnect.foundation.common.model.PublicKey import com.walletconnect.foundation.common.model.Topic import com.walletconnect.foundation.util.Logger import com.walletconnect.sign.common.exceptions.NotSettledSessionException -import com.walletconnect.sign.common.exceptions.UnauthorizedPeerException import com.walletconnect.sign.common.model.vo.sequence.SessionVO import com.walletconnect.sign.storage.sequence.SessionStorageRepository import io.mockk.every diff --git a/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCaseTest.kt b/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCaseTest.kt new file mode 100644 index 0000000000..a6e453ef8f --- /dev/null +++ b/protocol/sign/src/test/kotlin/com/walletconnect/sign/engine/use_case/calls/RejectSessionAuthenticateUseCaseTest.kt @@ -0,0 +1,130 @@ +package com.walletconnect.sign.engine.use_case.calls + +import com.walletconnect.android.internal.common.crypto.kmr.KeyManagementRepository +import com.walletconnect.android.internal.common.model.AppMetaData +import com.walletconnect.android.internal.common.model.EnvelopeType +import com.walletconnect.android.internal.common.model.SymmetricKey +import com.walletconnect.android.internal.common.model.type.JsonRpcInteractorInterface +import com.walletconnect.android.internal.common.storage.verify.VerifyContextStorageRepository +import com.walletconnect.android.internal.utils.fiveMinutesInSeconds +import com.walletconnect.foundation.common.model.PublicKey +import com.walletconnect.foundation.common.model.Topic +import com.walletconnect.foundation.util.Logger +import com.walletconnect.sign.common.exceptions.MissingSessionAuthenticateRequest +import com.walletconnect.sign.common.model.Request +import com.walletconnect.sign.common.model.vo.clientsync.common.PayloadParams +import com.walletconnect.sign.common.model.vo.clientsync.common.Requester +import com.walletconnect.sign.common.model.vo.clientsync.session.params.SignParams +import com.walletconnect.sign.json_rpc.domain.GetPendingSessionAuthenticateRequest +import com.walletconnect.util.bytesToHex +import com.walletconnect.util.randomBytes +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.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +class RejectSessionAuthenticateUseCaseTest { + private val jsonRpcInteractor: JsonRpcInteractorInterface = mockk() + private val getPendingSessionAuthenticateRequest: GetPendingSessionAuthenticateRequest = mockk() + private val crypto: KeyManagementRepository = mockk() + private val verifyContextStorageRepository: VerifyContextStorageRepository = mockk() + private val logger: Logger = mockk() + private lateinit var useCase: RejectSessionAuthenticateUseCase + private val testDispatcher = StandardTestDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + useCase = RejectSessionAuthenticateUseCase( + jsonRpcInteractor, + getPendingSessionAuthenticateRequest, + crypto, + verifyContextStorageRepository, + logger + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `rejectSessionAuthenticate should log error and call onFailure when request is missing`() = runTest(testDispatcher) { + val id = 1L + val reason = "Test reason" + coEvery { getPendingSessionAuthenticateRequest(id) } returns null + every { logger.error(MissingSessionAuthenticateRequest().message) } just Runs + val onFailure = mockk<(Throwable) -> Unit>(relaxed = true) + + useCase.rejectSessionAuthenticate(id, reason, {}, onFailure) + + verify { logger.error(MissingSessionAuthenticateRequest().message) } + verify { onFailure(any()) } + } + + @Test + fun `rejectSessionAuthenticate should publish response and call onSuccess when request is valid`() = runTest(testDispatcher) { + val id = 1L + val reason = "Test reason" + val sessionAuthenticateParams = SignParams.SessionAuthenticateParams( + expiryTimestamp = fiveMinutesInSeconds, + requester = Requester("receiverPublicKey", metadata = AppMetaData("name", "description", listOf("url"), "name")), + authPayload = PayloadParams( + type = "type", + aud = "aud", + version = "v", + iat = "iat", + chains = listOf("chains"), + domain = "sample.kotlin.dapp", + nonce = randomBytes(12).bytesToHex(), + exp = null, + nbf = null, + statement = "Sign in with wallet.", + requestId = null, + resources = listOf( + "urn:recap:eyJhdHQiOnsiaHR0cHM6Ly9ub3RpZnkud2FsbGV0Y29ubmVjdC5jb20vYWxsLWFwcHMiOnsiY3J1ZC9zdWJzY3JpcHRpb25zIjpbe31dLCJjcnVkL25vdGlmaWNhdGlvbnMiOlt7fV19fX0=", + "ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/" + ), + ) + ) + + val jsonRpcHistoryEntry = Request(chainId = "chainId", id = id, method = "method", topic = Topic("topic"), params = sessionAuthenticateParams) + val senderPublicKey = PublicKey("senderPublicKey") + val receiverPublicKey = PublicKey("receiverPublicKey") + val symmetricKey = SymmetricKey("symmetricKey") + val responseTopic = Topic("responseTopic") + coEvery { getPendingSessionAuthenticateRequest(id) } returns jsonRpcHistoryEntry + coEvery { crypto.generateAndStoreX25519KeyPair() } returns senderPublicKey + coEvery { crypto.generateSymmetricKeyFromKeyAgreement(senderPublicKey, receiverPublicKey) } returns symmetricKey + coEvery { crypto.getTopicFromKey(receiverPublicKey) } returns responseTopic + every { crypto.setKey(symmetricKey, responseTopic.value) } just Runs + coEvery { jsonRpcInteractor.publishJsonRpcResponse(any(), any(), any(), any(), any(), any(), any()) } just Runs + every { logger.log("Session Authenticate Reject Responded on topic: $responseTopic") } just Runs + every { logger.log("Sending Session Authenticate Reject on topic: $responseTopic") } just Runs + coEvery { verifyContextStorageRepository.delete(id) } just Runs + useCase.rejectSessionAuthenticate(id, reason, onSuccess = {}, onFailure = {}) + coVerify { + jsonRpcInteractor.publishJsonRpcResponse( + responseTopic, any(), any(), any(), any(), any(), EnvelopeType.ONE + ) + } +// coVerify { verifyContextStorageRepository.delete(any()) } +// verify { logger.log("Session Authenticate Reject Responded on topic: $responseTopic") } +// verify { onSuccess() } + } +} \ No newline at end of file