Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-05 16:08:09 +07:00
commit bf2f639cb2
10 changed files with 120 additions and 29 deletions

View file

@ -113,6 +113,7 @@ sealed class AnalyticsParam {
const val SEED_PHRASE_LENGTH = "Seed Phrase Length" const val SEED_PHRASE_LENGTH = "Seed Phrase Length"
const val DAPP_NAME = "DApp Name" const val DAPP_NAME = "DApp Name"
const val DAPP_URL = "DApp Url" const val DAPP_URL = "DApp Url"
const val NETWORKS = "Networks"
const val METHOD_NAME = "Method Name" const val METHOD_NAME = "Method Name"
const val VALIDATION = "Validation" const val VALIDATION = "Validation"
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.analytics.events package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -12,8 +13,41 @@ internal sealed class WalletConnect(
) : AnalyticsEvent("Wallet Connect", event, params, error) { ) : AnalyticsEvent("Wallet Connect", event, params, error) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened") class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionEstablished(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect( class NewSessionInitiated(source: SourceType) : WalletConnect(
event = "New Session Established", event = "Session Initiated",
params = mapOf(
AnalyticsParam.SOURCE to when (source) {
SourceType.QR -> "QR"
SourceType.DEEPLINK -> "DeepLink"
SourceType.ETC -> "etc"
},
),
)
data object SessionFailed : WalletConnect(
event = "Session Failed",
)
class DAppConnectionRequested(
blockchainNames: List<String>,
) : WalletConnect(
event = "dApp Connection Requested",
params = mapOf(
AnalyticsParam.NETWORKS to blockchainNames.joinToString(","),
),
)
class DAppConnected(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class DAppConnectionFailed(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connection Failed",
params = mapOf( params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName, AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl, AnalyticsParam.DAPP_URL to dAppUrl,
@ -22,17 +56,31 @@ internal sealed class WalletConnect(
) )
class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect( class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect(
event = "Session Disconnected", event = "dApp Disconnected",
params = mapOf( params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName, AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl, AnalyticsParam.DAPP_URL to dAppUrl,
), ),
) )
class RequestHandled( class SignatureRequestHandled(
params: RequestHandledParams, params: RequestHandledParams,
) : WalletConnect( ) : WalletConnect(
event = "Request Handled", event = "Signature Request Handled",
params = params.toParamsMap(),
)
class SignatureRequestReceived(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Received",
params = params.toParamsMap(),
)
class SignatureRequestFailed(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Failed",
params = params.toParamsMap(), params = params.toParamsMap(),
) )

View file

@ -9,11 +9,9 @@ import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -24,6 +22,7 @@ internal class DefaultLegacyWalletConnectRepository(
private val application: Application, private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer, private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler, private val analyticsHandler: AnalyticsEventHandler,
private val blockchainHelper: WcBlockchainHelper,
) : LegacyWalletConnectRepository { ) : LegacyWalletConnectRepository {
private var sessionProposal: Wallet.Model.SessionProposal? = null private var sessionProposal: Wallet.Model.SessionProposal? = null
@ -136,6 +135,13 @@ internal class DefaultLegacyWalletConnectRepository(
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(), userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
) )
val requiredChainIds = sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() }
val optionalChainIds = optionalWithoutMissingNetworks.toList()
val networks = (requiredChainIds + optionalChainIds)
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.distinct()
analyticsHandler.send(WalletConnect.DAppConnectionRequested(networks))
scope.launch { scope.launch {
_events.emit( _events.emit(
WalletConnectEvents.SessionProposal( WalletConnectEvents.SessionProposal(
@ -143,8 +149,8 @@ internal class DefaultLegacyWalletConnectRepository(
sessionProposal.description, sessionProposal.description,
sessionProposal.url, sessionProposal.url,
sessionProposal.icons, sessionProposal.icons,
sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() }, requiredChainIds,
optionalWithoutMissingNetworks.toList(), optionalChainIds,
), ),
) )
} }
@ -176,7 +182,17 @@ internal class DefaultLegacyWalletConnectRepository(
result = "", result = "",
) )
} }
else -> else -> {
val event = WalletConnect.SignatureRequestReceived(
WalletConnect.RequestHandledParams(
dAppName = sessionRequest.peerMetaData?.name ?: "",
dAppUrl = sessionRequest.peerMetaData?.url ?: "",
methodName = sessionRequest.request.method,
blockchain = sessionRequest.chainId
?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "",
),
)
analyticsHandler.send(event)
scope.launch { scope.launch {
_events.emit( _events.emit(
WalletConnectEvents.SessionRequest( WalletConnectEvents.SessionRequest(
@ -190,6 +206,7 @@ internal class DefaultLegacyWalletConnectRepository(
), ),
) )
} }
}
} }
} }
@ -247,7 +264,8 @@ internal class DefaultLegacyWalletConnectRepository(
this.userNamespaces = userNamespaces this.userNamespaces = userNamespaces
} }
override fun pair(uri: String) { override fun pair(uri: String, source: SourceType) {
analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair( WalletKit.pair(
params = Wallet.Params.Pair(uri), params = Wallet.Params.Pair(uri),
onSuccess = { onSuccess = {
@ -255,6 +273,7 @@ internal class DefaultLegacyWalletConnectRepository(
}, },
onError = { onError = {
Timber.e("Error while pairing: $it") Timber.e("Error while pairing: $it")
analyticsHandler.send(WalletConnect.SessionFailed)
scope.launch { scope.launch {
_events.emit( _events.emit(
WalletConnectEvents.PairConnectError(it.throwable), WalletConnectEvents.PairConnectError(it.throwable),
@ -305,7 +324,7 @@ internal class DefaultLegacyWalletConnectRepository(
onSuccess = { onSuccess = {
Timber.i("Approved successfully: $it") Timber.i("Approved successfully: $it")
analyticsHandler.send( analyticsHandler.send(
WalletConnect.NewSessionEstablished( WalletConnect.DAppConnected(
dAppName = sessionProposal.name, dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url, dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames, blockchainNames = blockchainNames,
@ -314,6 +333,13 @@ internal class DefaultLegacyWalletConnectRepository(
}, },
onError = { onError = {
Timber.e("Error while approving: $it") Timber.e("Error while approving: $it")
analyticsHandler.send(
WalletConnect.DAppConnectionFailed(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
scope.launch { scope.launch {
_events.emit( _events.emit(
WalletConnectEvents.SessionApprovalError( WalletConnectEvents.SessionApprovalError(
@ -352,7 +378,7 @@ internal class DefaultLegacyWalletConnectRepository(
// Add Ethereum Chain method is processed without user input, skip logging it // Add Ethereum Chain method is processed without user input, skip logging it
if (requestData.method != WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code) { if (requestData.method != WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code) {
analyticsHandler.send( analyticsHandler.send(
WalletConnect.RequestHandled( WalletConnect.SignatureRequestHandled(
WalletConnect.RequestHandledParams( WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "", dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "", dAppUrl = session?.url ?: "",
@ -377,7 +403,7 @@ internal class DefaultLegacyWalletConnectRepository(
onError = { error -> onError = { error ->
Timber.e(error.throwable, "Error while responging session request") Timber.e(error.throwable, "Error while responging session request")
WalletConnect.RequestHandledParams( val params = WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "", dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "", dAppUrl = session?.url ?: "",
methodName = requestData.method, methodName = requestData.method,
@ -385,6 +411,7 @@ internal class DefaultLegacyWalletConnectRepository(
errorCode = WalletConnectError.ValidationError.error, errorCode = WalletConnectError.ValidationError.error,
errorDescription = error.throwable.message, errorDescription = error.throwable.message,
) )
analyticsHandler.send(WalletConnect.SignatureRequestFailed(params))
}, },
) )
} }
@ -393,7 +420,7 @@ internal class DefaultLegacyWalletConnectRepository(
val session = currentSessions.find { it.topic == requestData.topic } val session = currentSessions.find { it.topic == requestData.topic }
analyticsHandler.send( analyticsHandler.send(
WalletConnect.RequestHandled( WalletConnect.SignatureRequestHandled(
WalletConnect.RequestHandledParams( WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "", dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "", dAppUrl = session?.url ?: "",

View file

@ -14,9 +14,7 @@ import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -35,7 +33,7 @@ internal object WalletConnectInteractorModule {
fun provideWalletConnectInteractor( fun provideWalletConnectInteractor(
wcRepository: LegacyWalletConnectRepository, wcRepository: LegacyWalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository, wcSessionsRepository: WalletConnectSessionsRepository,
walletConnectFeatureToggles: WalletConnectFeatureToggles, wcBlockchainHelper: WcBlockchainHelper,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
@ -46,7 +44,7 @@ internal object WalletConnectInteractorModule {
walletConnectRepository = wcRepository, walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository, sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(), sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles), blockchainHelper = wcBlockchainHelper,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
@ -65,17 +63,25 @@ internal object WalletConnectModule {
return WalletConnectFeatureToggles(featureTogglesManager) return WalletConnectFeatureToggles(featureTogglesManager)
} }
@Provides
@Singleton
fun provideWcBlockchainHelper(walletConnectFeatureToggles: WalletConnectFeatureToggles): WcBlockchainHelper {
return TangemWcBlockchainHelper(walletConnectFeatureToggles)
}
@Provides @Provides
@Singleton @Singleton
fun provideWalletConnectRepository( fun provideWalletConnectRepository(
application: Application, application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer, wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler, analyticsHandler: AnalyticsEventHandler,
wcBlockchainHelper: WcBlockchainHelper,
): LegacyWalletConnectRepository { ): LegacyWalletConnectRepository {
return DefaultLegacyWalletConnectRepository( return DefaultLegacyWalletConnectRepository(
application = application, application = application,
wcRequestDeserializer = wcRequestDeserializer, wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler, analyticsHandler = analyticsHandler,
blockchainHelper = wcBlockchainHelper,
) )
} }

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.walletconnect2.domain package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface LegacyWalletConnectRepository { interface LegacyWalletConnectRepository {
@ -17,7 +18,7 @@ interface LegacyWalletConnectRepository {
fun updateSessions() fun updateSessions()
fun pair(uri: String) fun pair(uri: String, source: SourceType)
fun disconnect(topic: String) fun disconnect(topic: String)

View file

@ -135,7 +135,9 @@ class WalletConnectInteractor(
isWalletConnectReadyForDeepLinks = true isWalletConnectReadyForDeepLinks = true
if (deeplinkStack.empty()) return if (deeplinkStack.empty()) return
val lastDeeplink = deeplinkStack.pop() val lastDeeplink = deeplinkStack.pop()
store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink)) val action = WalletConnectAction
.OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
store.dispatchOnMain(action)
}.onFailure { }.onFailure {
Timber.e("WC deeplink handling failed. $it") Timber.e("WC deeplink handling failed. $it")
} }
@ -392,7 +394,8 @@ class WalletConnectInteractor(
} }
if (isWalletConnectReadyForDeepLinks) { if (isWalletConnectReadyForDeepLinks) {
store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink)) val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
store.dispatchOnMain(action)
} else { } else {
deeplinkStack.push(deeplink) deeplinkStack.push(deeplink)
} }

View file

@ -15,7 +15,10 @@ sealed class WalletConnectAction : Action {
data class OpenSession( data class OpenSession(
val wcUri: String, val wcUri: String,
) : WalletConnectAction() val source: SourceType,
) : WalletConnectAction() {
enum class SourceType { QR, DEEPLINK, ETC }
}
data class DisconnectSession(val topic: String) : WalletConnectAction() data class DisconnectSession(val topic: String) : WalletConnectAction()

View file

@ -76,7 +76,7 @@ class WalletConnectMiddleware {
is WalletConnectAction.OpenSession -> { is WalletConnectAction.OpenSession -> {
val index = action.wcUri.indexOf("@") val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) { when (action.wcUri[index + 1]) {
'2' -> walletConnectRepository.pair(uri = action.wcUri) '2' -> walletConnectRepository.pair(uri = action.wcUri, source = action.source)
'1' -> { '1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain( store.dispatchOnMain(

View file

@ -13,6 +13,7 @@ import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -29,7 +30,8 @@ internal class WalletConnectModel @Inject constructor(
modelScope.launch { modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT) listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() } .getOrElse { emptyFlow() }
.collect { store.dispatch(WalletConnectAction.OpenSession(it)) } .map { WalletConnectAction.OpenSession(it, WalletConnectAction.OpenSession.SourceType.QR) }
.collect { store.dispatch(it) }
} }
} }

View file

@ -17,7 +17,7 @@ object ClipboardOrScanQrDialog {
setTitle(context.getString(R.string.common_select_action)) setTitle(context.getString(R.string.common_select_action))
setMessage(context.getText(R.string.wallet_connect_clipboard_alert)) setMessage(context.getText(R.string.wallet_connect_clipboard_alert))
setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ -> setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ ->
store.dispatch(WalletConnectAction.OpenSession(wcUri)) store.dispatch(WalletConnectAction.OpenSession(wcUri, WalletConnectAction.OpenSession.SourceType.ETC))
} }
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ -> setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
store.dispatchNavigationAction { store.dispatchNavigationAction {