diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt index 762b7afb71..a231426ebb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -31,12 +32,18 @@ internal class WalletConnectModel @Inject constructor( init { modelScope.launch { - listenToQrScanningUseCase(SourceType.WALLET_CONNECT) + listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .map { + .map { result -> + val source = when (result.resultSource) { + QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD + QrResultSource.CAMERA, + QrResultSource.GALLERY, + -> WalletConnectAction.OpenSession.SourceType.QR + } WalletConnectAction.OpenSession( - wcUri = it, - source = WalletConnectAction.OpenSession.SourceType.QR, + wcUri = result.qrCode, + source = source, userWalletId = params.userWalletId, ) } diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 5b68110f08..99ad446932 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -17,17 +18,17 @@ import java.net.URLDecoder internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { - private data class QrScanningEvent(val type: SourceType, val qrCode: String) + private data class QrScanningEvent(val qrCode: RawQrResult) private val scannedEvents = MutableSharedFlow(replay = 1) - override suspend fun emitResult(type: SourceType, qrCode: String) { - scannedEvents.emit(QrScanningEvent(type, qrCode)) + override suspend fun emitResult(qrCode: RawQrResult) { + scannedEvents.emit(QrScanningEvent(qrCode)) } @OptIn(ExperimentalCoroutinesApi::class) override fun subscribeToScanningResults(type: SourceType) = scannedEvents - .filter { it.type == type } + .filter { it.qrCode.requestSource == type } .map { it.qrCode } .onEach { yield() // if we have more than one sub, we must allow them to collect emitted value diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 8e39df4b21..0143e8b307 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -72,7 +72,9 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun defaultWcPairUseCase(): WcPairService = DefaultWcPairService() + fun defaultWcPairUseCase(sessionsManager: DefaultWcSessionsManager): WcPairService = DefaultWcPairService( + sessionsManager, + ) @Provides @Singleton @@ -90,6 +92,7 @@ internal object WalletConnectDataModule { legacyStore: WalletConnectSessionsRepository, getWallets: GetWalletsUseCase, associateNetworks: AssociateNetworksDelegate, + analytics: AnalyticsEventHandler, ): DefaultWcSessionsManager { val scope = CoroutineScope(SupervisorJob() + dispatchers.io) return DefaultWcSessionsManager( @@ -98,6 +101,7 @@ internal object WalletConnectDataModule { legacyStore = legacyStore, getWallets = getWallets, associateNetworks = associateNetworks, + analytics = analytics, scope = scope, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index e8de784b36..0621f51855 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData -import com.tangem.blockchain.common.Amount as BlockchainAmount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -25,13 +24,16 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* +import com.tangem.blockchain.common.Amount as BlockchainAmount +@Suppress("LongParameterList") internal class WcEthSendTransactionUseCase @AssistedInject constructor( @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SendTransaction, override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, private val sendTransaction: SendTransactionUseCase, + private val ethTxHelper: WcEthTxHelper, blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), WcTransactionUseCase, @@ -39,10 +41,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( WcMutableFee { private var approvalAmount: WcApprovedAmount? = null - private var dAppFee = WcEthTxHelper.getDAppFee( - network = context.network, - txParams = method.transaction, - ) + private var dAppFee: Fee? = null override val securityStatus: LceFlow = blockAidDelegate.getSecurityStatus( @@ -53,7 +52,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( accountAddress = context.accountAddress, ).map { lce -> lce.map { result -> - val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) + val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result) ?: return@map BlockAidTransactionCheck.Result.Plain(result) val tokenInfo = amount.tokenInfo this@WcEthSendTransactionUseCase.approvalAmount = WcApprovedAmount( @@ -112,7 +111,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emit(newState) } - override fun dAppFee(): Fee.Ethereum.Legacy? { + override fun dAppFee(): Fee? { return dAppFee } @@ -121,8 +120,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( } override fun invoke(): Flow> = flow { - val transactionData = WcEthTxHelper.createTransactionData( - dAppFee = dAppFee(), + dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network) + val transactionData = ethTxHelper.createTransactionData( + dAppFee = dAppFee, network = context.network, txParams = method.transaction, ) ?: return@flow diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index bb9de17e6e..eda9598750 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData -import com.tangem.blockchain.common.Amount as BlockchainAmount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -24,16 +23,15 @@ import com.tangem.domain.walletconnect.usecase.method.* import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* +import com.tangem.blockchain.common.Amount as BlockchainAmount +@Suppress("LongParameterList") internal class WcEthSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, private val prepareForSend: PrepareForSendUseCase, + private val ethTxHelper: WcEthTxHelper, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTransaction, blockAidDelegate: BlockAidVerificationDelegate, @@ -43,10 +41,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( WcMutableFee { private var approvalAmount: WcApprovedAmount? = null - private var dAppFee = WcEthTxHelper.getDAppFee( - network = context.network, - txParams = method.transaction, - ) + private var dAppFee: Fee? = null override val securityStatus = blockAidDelegate.getSecurityStatus( network = network, @@ -56,7 +51,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( accountAddress = context.accountAddress, ).map { lce -> lce.map { result -> - val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) + val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result) ?: return@map BlockAidTransactionCheck.Result.Plain(result) val tokenInfo = amount.tokenInfo if (!amount.isUnlimited) { @@ -120,15 +115,16 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( } override fun invoke(): Flow> = flow { - val transactionData = WcEthTxHelper.createTransactionData( - dAppFee = dAppFee(), + dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network) + val transactionData = ethTxHelper.createTransactionData( + dAppFee = dAppFee, network = context.network, txParams = method.transaction, ) ?: return@flow emitAll(delegate.invoke(transactionData)) } - override fun dAppFee(): Fee.Ethereum.Legacy? { + override fun dAppFee(): Fee? { return dAppFee } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index 268633a213..59e52249da 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -7,40 +7,43 @@ import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.HEX_PREFIX import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.hexToBigDecimal +import com.tangem.blockchain.extensions.hexToBigInteger import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.common.extensions.hexToBytes +import com.tangem.data.common.currency.getCoinId import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthTransactionParams -import java.math.BigDecimal +import com.tangem.domain.wallets.models.UserWallet +import javax.inject.Inject -internal object WcEthTxHelper { - private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8") +internal class WcEthTxHelper @Inject constructor( + private val getSingleCryptoCurrency: GetSingleCryptoCurrencyStatusUseCase, + private val ethSpecificFee: GetEthSpecificFeeUseCase, +) { - fun getDAppFee(network: Network, txParams: WcEthTransactionParams): Fee.Ethereum.Legacy? { - val gasLimit = txParams.gas?.hexToBigDecimal() ?: return null - val gasPrice = txParams.gasPrice?.hexToBigDecimal() ?: return null - - val blockchain = network.toBlockchain() - - var feeDecimal = (gasLimit * gasPrice) - .movePointLeft(blockchain.decimals()) - if (blockchain == Blockchain.Mantle) { - feeDecimal = feeDecimal.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER) - } - - val feeAmount = Amount(feeDecimal, blockchain) - return Fee.Ethereum.Legacy(feeAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) + suspend fun getDAppFee(txParams: WcEthTransactionParams, userWallet: UserWallet, network: Network): Fee? { + val gasLimit = txParams.gas?.hexToBigInteger() ?: return null + val gasPrice = txParams.gasPrice?.hexToBigInteger() + val coinId = getCoinId(network, network.toBlockchain().toCoinId()) + val currency = getSingleCryptoCurrency.invokeMultiWalletSync(userWallet.walletId, coinId) + .map { it.currency } + .getOrNull() ?: return null + return ethSpecificFee(userWallet, currency, gasLimit, gasPrice) + .map { it.minimum } + .getOrNull() } fun createTransactionData( - dAppFee: Fee.Ethereum.Legacy?, + dAppFee: Fee?, network: Network, txParams: WcEthTransactionParams, ): TransactionData.Uncompiled? { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt index bb0c819b4b..d0749f0362 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt @@ -1,17 +1,45 @@ package com.tangem.data.walletconnect.pair +import androidx.core.net.toUri import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.repository.WcSessionsManager import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.receiveAsFlow import javax.inject.Inject -class DefaultWcPairService @Inject constructor() : WcPairService { +class DefaultWcPairService @Inject constructor( + private val sessionsManager: WcSessionsManager, +) : WcPairService { private val _pairFlow: Channel = Channel(Channel.BUFFERED) - override val pairFlow: Flow = _pairFlow.receiveAsFlow() + override val pairFlow: Flow = _pairFlow + .receiveAsFlow() + .filter(::filterDeeplinkRequestWithSessionTopic) override fun pair(request: WcPairRequest) { _pairFlow.trySend(request) } + + // some dApp sends deeplink with session request + // we filter session exist, but start dApp pair flow if unexist + private suspend fun filterDeeplinkRequestWithSessionTopic(request: WcPairRequest): Boolean { + when (request.source) { + WcPairRequest.Source.QR, + WcPairRequest.Source.CLIPBOARD, + WcPairRequest.Source.ETC, + -> return true + WcPairRequest.Source.DEEPLINK -> Unit + } + + val isExistSession = existSessionTopic(request.uri).getOrNull() ?: false + return !isExistSession + } + + private suspend fun existSessionTopic(uri: String) = runCatching { + val sessionTopic = uri.toUri().getQueryParameter("sessionTopic") ?: return@runCatching false + val isExistSession = sessionsManager.findSessionByTopic(sessionTopic) != null + return@runCatching isExistSession + } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 885f896a41..eb0714e97c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -8,10 +8,10 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -37,6 +37,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( private val onCallTerminalAction = Channel() + @Suppress("LongMethod") override operator fun invoke(): Flow { val (uri: String, source: WcPairRequest.Source) = pairRequest return flow { @@ -47,6 +48,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( val sdkSessionProposal = sdkDelegate.pair(uri) .onLeft { Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest") + analytics.send(WcAnalyticEvents.PairFailed) emit(WcPairState.Error(it)) } .getOrNull() ?: return@flow @@ -76,7 +78,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } // finish flow if rejected above if (sessionForApprove == null) { - analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession)) + analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession.dAppMetaData)) sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) return@flow } @@ -155,7 +157,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( analytics.send( WcAnalyticEvents.PairRequested( network = requestedNetworks, - verificationInfo.name, + verificationInfo, ), ) val appMetaData = WcAppMetaData( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index b82c70ec75..72a227886b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -6,11 +6,13 @@ import arrow.core.right import com.domain.blockaid.models.dapp.CheckDAppResult import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.datasource.local.walletconnect.WalletConnectStore +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository @@ -24,12 +26,14 @@ import kotlinx.coroutines.flow.* import timber.log.Timber import kotlin.coroutines.resume +@Suppress("LongParameterList") internal class DefaultWcSessionsManager( private val store: WalletConnectStore, private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, private val dispatchers: CoroutineDispatcherProvider, private val associateNetworks: AssociateNetworksDelegate, + private val analytics: AnalyticsEventHandler, private val scope: CoroutineScope, ) : WcSessionsManager, WcSdkObserver { @@ -68,6 +72,7 @@ internal class DefaultWcSessionsManager( val topic = session.sdkModel.topic val sdkCall = sdkDisconnectSession(topic) .onRight { onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = "")) } + analytics.send(WcAnalyticEvents.SessionDisconnected(session.sdkModel.appMetaData)) return sdkCall } diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt index a97a4a7161..af9fb4f3ec 100644 --- a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt @@ -6,4 +6,14 @@ data class QrResult( var address: String = "", var amount: BigDecimal? = null, var memo: String? = null, -) \ No newline at end of file +) + +data class RawQrResult( + val qrCode: String, + val resultSource: QrResultSource, + val requestSource: SourceType, +) + +enum class QrResultSource { + CLIPBOARD, CAMERA, GALLERY +} \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt index f24ef288bf..323e57d915 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt @@ -2,14 +2,15 @@ package com.tangem.domain.qrscanning.repository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.models.SourceType import kotlinx.coroutines.flow.Flow interface QrScanningEventsRepository { - suspend fun emitResult(type: SourceType, qrCode: String) + suspend fun emitResult(qrCode: RawQrResult) - fun subscribeToScanningResults(type: SourceType): Flow + fun subscribeToScanningResults(type: SourceType): Flow fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult } \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt index ebe0e5980f..872a95af6c 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt @@ -3,15 +3,15 @@ package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository class EmitQrScannedEventUseCase( private val repository: QrScanningEventsRepository, ) { - suspend operator fun invoke(type: SourceType, qrCode: String): Either { + suspend operator fun invoke(qrCode: RawQrResult): Either { return try { - repository.emitResult(type, qrCode) + repository.emitResult(qrCode) Unit.right() } catch (e: Exception) { e.left() diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt index 3c42400420..dd1157a899 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt @@ -3,15 +3,25 @@ package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map class ListenToQrScanningUseCase( val repository: QrScanningEventsRepository, ) { operator fun invoke(type: SourceType): Either> { + return try { + repository.subscribeToScanningResults(type).map { it.qrCode }.right() + } catch (e: Exception) { + e.left() + } + } + + fun listen(type: SourceType): Either> { return try { repository.subscribeToScanningResults(type).right() } catch (e: Exception) { diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 37b4175437..cb8f983b1f 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -1,5 +1,7 @@ package com.tangem.domain.walletconnect +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.CheckDAppResult.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.models.network.Network @@ -7,6 +9,7 @@ import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest sealed class WcAnalyticEvents( @@ -14,6 +17,7 @@ sealed class WcAnalyticEvents( params: Map = mapOf(), ) : AnalyticsEvent(category = "Wallet Connect", event = event, params = params) { + object ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( event = "Session Initiated", params = mapOf( @@ -32,12 +36,16 @@ sealed class WcAnalyticEvents( class PairRequested( network: Set, - domainVerification: String, + domainVerification: CheckDAppResult, ) : WcAnalyticEvents( event = "dApp Connection Requested", params = mapOf( NETWORKS to network.joinToString(",") { it.name }, - DOMAIN_VERIFICATION to domainVerification, + DOMAIN_VERIFICATION to when (domainVerification) { + SAFE -> "Verified" + UNSAFE -> "Risky" + FAILED_TO_VERIFY -> "Unknown" + }, ), ) @@ -66,11 +74,11 @@ sealed class WcAnalyticEvents( ), ) - class SessionDisconnected(sessionProposal: WcSessionProposal) : WcAnalyticEvents( + class SessionDisconnected(dAppMetaData: WcAppMetaData) : WcAnalyticEvents( event = "dApp Disconnected", params = mapOf( - AnalyticsParam.Key.DAPP_NAME to sessionProposal.dAppMetaData.name, - AnalyticsParam.Key.DAPP_URL to sessionProposal.dAppMetaData.url, + AnalyticsParam.Key.DAPP_NAME to dAppMetaData.name, + AnalyticsParam.Key.DAPP_URL to dAppMetaData.url, ), ) @@ -155,6 +163,30 @@ sealed class WcAnalyticEvents( ), ) + class NoticeSecurityAlert( + dAppMetaData: WcAppMetaData, + securityStatus: CheckDAppResult, + source: Source, + ) : WcAnalyticEvents( + event = "Notice - Security Alert", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to dAppMetaData.name, + AnalyticsParam.Key.DAPP_URL to dAppMetaData.url, + AnalyticsParam.Key.SOURCE to when (source) { + Source.Domain -> "Domain" + Source.SmartContract -> "Smart Contract" + }, + AnalyticsParam.Key.TYPE to when (securityStatus) { + SAFE -> "Verified" + UNSAFE -> "Risky" + FAILED_TO_VERIFY -> "Unknown" + }, + + ), + ) { + enum class Source { Domain, SmartContract } + } + companion object { const val NETWORKS = "Networks" const val DOMAIN_VERIFICATION = "Domain Verification" diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt index 2e00a6910d..8ee7c7ea48 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt @@ -18,6 +18,7 @@ import com.google.mlkit.vision.common.InputImage import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.SystemBarsIconsDisposable +import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.model.QrScanningModel import com.tangem.feature.qrscanning.presentation.QrScanningContent @@ -41,10 +42,10 @@ class DefaultQrScanningComponent @AssistedInject constructor( // Camera requires its own analyzer instance due to flow of frames needed to be analyzed. // Each new frame can cancel previous analysis e.i. image from the gallery can be skipped. private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) { - MLKitBarcodeAnalyzer(model::onQrScanned) + MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.CAMERA) } } private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) { - MLKitBarcodeAnalyzer(model::onQrScanned) + MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.GALLERY) } } init { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt index b0702b0d62..c44a3c6fcd 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.qrscanning.model +import com.tangem.domain.qrscanning.models.QrResultSource import kotlinx.coroutines.flow.SharedFlow internal interface QrScanningClickIntents { @@ -8,7 +9,7 @@ internal interface QrScanningClickIntents { fun onBackClick() - fun onQrScanned(qrCode: String) + fun onQrScanned(qrCode: String, source: QrResultSource) fun onGalleryClicked() diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt index 505a42e7ec..54f9af36c5 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt @@ -10,6 +10,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.domain.qrscanning.models.QrResultSource +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.qrscanning.presentation.QrScanningState @@ -71,10 +73,11 @@ internal class QrScanningModel @Inject constructor( override fun onBackClick() = appRouter.pop() - override fun onQrScanned(qrCode: String) { + override fun onQrScanned(qrCode: String, source: QrResultSource) { if (qrCode.isNotBlank()) { modelScope.launch(dispatchers.mainImmediate) { - emitQrScannedEventUseCase.invoke(params.source, qrCode) + val qrCode = RawQrResult(qrCode, source, params.source) + emitQrScannedEventUseCase.invoke(qrCode) } if (!isScanned) { appRouter.pop() diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index 1b51ac2222..bc9659cc28 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.impl.R import com.tangem.feature.qrscanning.model.QrScanningClickIntents @@ -28,7 +29,7 @@ internal class InitializeQrScanningStateTransformer( topBarConfig = constructTopBarConfig(), message = message, onBackClick = clickIntents::onBackClick, - onQrScanned = clickIntents::onQrScanned, + onQrScanned = { qrCode -> clickIntents.onQrScanned(qrCode, QrResultSource.CAMERA) }, onGalleryClick = clickIntents::onGalleryClicked, pasteAction = constructPasteAction(), ) @@ -50,7 +51,7 @@ internal class InitializeQrScanningStateTransformer( private fun constructPasteAction(): PasteAction { val uri = clipboardManager.getText() return if (uri != null) { - PasteAction.Perform { clickIntents.onQrScanned(uri) } + PasteAction.Perform { clickIntents.onQrScanned(uri, QrResultSource.CLIPBOARD) } } else { PasteAction.None } diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index f0cd4ec491..9bccd9b7a3 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.common.routing) implementation(projects.common.ui) + implementation(projects.core.analytics) /** Domain models */ implementation(projects.domain.appCurrency.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 7dcf11a07d..d027a899cb 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -19,6 +20,7 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession @@ -47,6 +49,7 @@ internal class WcConnectionsModel @Inject constructor( private val wcDisconnectUseCase: WcDisconnectUseCase, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, + private val analytics: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -56,6 +59,7 @@ internal class WcConnectionsModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { + analytics.send(WcAnalyticEvents.ScreenOpened) listenQrUpdates() listenWcSessions() } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 40353377db..dab4eecca2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.connections.model import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.stack.* import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -11,6 +12,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -42,10 +44,12 @@ private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2 @Stable @ModelScoped +@Suppress("LongParameterList") internal class WcPairModel @Inject constructor( private val router: Router, private val messageSender: UiMessageSender, override val dispatchers: CoroutineDispatcherProvider, + private val analytics: AnalyticsEventHandler, wcPairUseCaseFactory: WcPairUseCase.Factory, getWalletsUseCase: GetWalletsUseCase, paramsContainer: ParamsContainer, @@ -165,10 +169,22 @@ internal class WcPairModel @Inject constructor( } private fun showUnknownDomainAlert() { + val event = WcAnalyticEvents.NoticeSecurityAlert( + dAppMetaData = sessionProposal.dAppMetaData, + securityStatus = sessionProposal.securityStatus, + source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain, + ) + analytics.send(event) stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnknownDomain)) } private fun showSecurityRiskAlert() { + val event = WcAnalyticEvents.NoticeSecurityAlert( + dAppMetaData = sessionProposal.dAppMetaData, + securityStatus = sessionProposal.securityStatus, + source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain, + ) + analytics.send(event) stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnsafeDomain)) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index ddcada687e..23e9e56b77 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -8,6 +8,7 @@ import com.arkivanov.decompose.router.stack.pushNew import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -21,6 +22,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcRequestError.Companion.message @@ -58,6 +60,7 @@ internal class WcSendTransactionModel @Inject constructor( private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter, private val getFeeUseCase: GetFeeUseCase, private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase, + private val analytics: AnalyticsEventHandler, ) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback { private val params = paramsContainer.require() @@ -219,6 +222,19 @@ internal class WcSendTransactionModel @Inject constructor( } else { sign() } + securityCheck?.result?.validation?.let { securityStatus -> + val event = WcAnalyticEvents.NoticeSecurityAlert( + dAppMetaData = useCase.session.sdkModel.appMetaData, + securityStatus = useCase.session.securityStatus, + source = WcAnalyticEvents.NoticeSecurityAlert.Source.SmartContract, + ) + when (securityStatus) { + ValidationResult.SAFE -> Unit + ValidationResult.UNSAFE, + ValidationResult.FAILED_TO_VALIDATE, + -> analytics.send(event) + } + } } fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) {