Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-03 23:49:10 +07:00
parent b96d11cea7
commit 2fb0ef1936
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 DAPP_NAME = "DApp Name"
const val DAPP_URL = "DApp Url"
const val NETWORKS = "Networks"
const val METHOD_NAME = "Method Name"
const val VALIDATION = "Validation"
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
/**
[REDACTED_AUTHOR]
@ -12,8 +13,41 @@ internal sealed class WalletConnect(
) : AnalyticsEvent("Wallet Connect", event, params, error) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionEstablished(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "New Session Established",
class NewSessionInitiated(source: SourceType) : WalletConnect(
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(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
@ -22,17 +56,31 @@ internal sealed class WalletConnect(
)
class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect(
event = "Session Disconnected",
event = "dApp Disconnected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
),
)
class RequestHandled(
class SignatureRequestHandled(
params: RequestHandledParams,
) : 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(),
)

View file

@ -9,11 +9,9 @@ import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
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.*
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.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -24,6 +22,7 @@ internal class DefaultLegacyWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
private val blockchainHelper: WcBlockchainHelper,
) : LegacyWalletConnectRepository {
private var sessionProposal: Wallet.Model.SessionProposal? = null
@ -136,6 +135,13 @@ internal class DefaultLegacyWalletConnectRepository(
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 {
_events.emit(
WalletConnectEvents.SessionProposal(
@ -143,8 +149,8 @@ internal class DefaultLegacyWalletConnectRepository(
sessionProposal.description,
sessionProposal.url,
sessionProposal.icons,
sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() },
optionalWithoutMissingNetworks.toList(),
requiredChainIds,
optionalChainIds,
),
)
}
@ -176,7 +182,17 @@ internal class DefaultLegacyWalletConnectRepository(
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 {
_events.emit(
WalletConnectEvents.SessionRequest(
@ -190,6 +206,7 @@ internal class DefaultLegacyWalletConnectRepository(
),
)
}
}
}
}
@ -247,7 +264,8 @@ internal class DefaultLegacyWalletConnectRepository(
this.userNamespaces = userNamespaces
}
override fun pair(uri: String) {
override fun pair(uri: String, source: SourceType) {
analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair(
params = Wallet.Params.Pair(uri),
onSuccess = {
@ -255,6 +273,7 @@ internal class DefaultLegacyWalletConnectRepository(
},
onError = {
Timber.e("Error while pairing: $it")
analyticsHandler.send(WalletConnect.SessionFailed)
scope.launch {
_events.emit(
WalletConnectEvents.PairConnectError(it.throwable),
@ -305,7 +324,7 @@ internal class DefaultLegacyWalletConnectRepository(
onSuccess = {
Timber.i("Approved successfully: $it")
analyticsHandler.send(
WalletConnect.NewSessionEstablished(
WalletConnect.DAppConnected(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
@ -314,6 +333,13 @@ internal class DefaultLegacyWalletConnectRepository(
},
onError = {
Timber.e("Error while approving: $it")
analyticsHandler.send(
WalletConnect.DAppConnectionFailed(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
@ -352,7 +378,7 @@ internal class DefaultLegacyWalletConnectRepository(
// Add Ethereum Chain method is processed without user input, skip logging it
if (requestData.method != WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code) {
analyticsHandler.send(
WalletConnect.RequestHandled(
WalletConnect.SignatureRequestHandled(
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
@ -377,7 +403,7 @@ internal class DefaultLegacyWalletConnectRepository(
onError = { error ->
Timber.e(error.throwable, "Error while responging session request")
WalletConnect.RequestHandledParams(
val params = WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
@ -385,6 +411,7 @@ internal class DefaultLegacyWalletConnectRepository(
errorCode = WalletConnectError.ValidationError.error,
errorDescription = error.throwable.message,
)
analyticsHandler.send(WalletConnect.SignatureRequestFailed(params))
},
)
}
@ -393,7 +420,7 @@ internal class DefaultLegacyWalletConnectRepository(
val session = currentSessions.find { it.topic == requestData.topic }
analyticsHandler.send(
WalletConnect.RequestHandled(
WalletConnect.SignatureRequestHandled(
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
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.data.DefaultLegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -35,7 +33,7 @@ internal object WalletConnectInteractorModule {
fun provideWalletConnectInteractor(
wcRepository: LegacyWalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
wcBlockchainHelper: WcBlockchainHelper,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
@ -46,7 +44,7 @@ internal object WalletConnectInteractorModule {
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles),
blockchainHelper = wcBlockchainHelper,
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
@ -65,17 +63,25 @@ internal object WalletConnectModule {
return WalletConnectFeatureToggles(featureTogglesManager)
}
@Provides
@Singleton
fun provideWcBlockchainHelper(walletConnectFeatureToggles: WalletConnectFeatureToggles): WcBlockchainHelper {
return TangemWcBlockchainHelper(walletConnectFeatureToggles)
}
@Provides
@Singleton
fun provideWalletConnectRepository(
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
wcBlockchainHelper: WcBlockchainHelper,
): LegacyWalletConnectRepository {
return DefaultLegacyWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
blockchainHelper = wcBlockchainHelper,
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -29,7 +30,8 @@ internal class WalletConnectModel @Inject constructor(
modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.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))
setMessage(context.getText(R.string.wallet_connect_clipboard_alert))
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)) { _, _ ->
store.dispatchNavigationAction {