Updated on 2026-08-14
This commit is contained in:
parent
9e38389156
commit
7493e56a52
22 changed files with 216 additions and 74 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<QrScanningEvent>(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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WcEthTxAction, TransactionData>(),
|
||||
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<Throwable, BlockAidTransactionCheck.Result> =
|
||||
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<WcSignState<TransactionData>> = 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
|
||||
|
|
|
|||
|
|
@ -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<WcSignState<TransactionData>> = 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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<WcPairRequest> = Channel(Channel.BUFFERED)
|
||||
override val pairFlow: Flow<WcPairRequest> = _pairFlow.receiveAsFlow()
|
||||
override val pairFlow: Flow<WcPairRequest> = _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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TerminalAction>()
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override operator fun invoke(): Flow<WcPairState> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,14 @@ data class QrResult(
|
|||
var address: String = "",
|
||||
var amount: BigDecimal? = null,
|
||||
var memo: String? = null,
|
||||
)
|
||||
)
|
||||
|
||||
data class RawQrResult(
|
||||
val qrCode: String,
|
||||
val resultSource: QrResultSource,
|
||||
val requestSource: SourceType,
|
||||
)
|
||||
|
||||
enum class QrResultSource {
|
||||
CLIPBOARD, CAMERA, GALLERY
|
||||
}
|
||||
|
|
@ -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<String>
|
||||
fun subscribeToScanningResults(type: SourceType): Flow<RawQrResult>
|
||||
|
||||
fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult
|
||||
}
|
||||
|
|
@ -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<Exception, Unit> {
|
||||
suspend operator fun invoke(qrCode: RawQrResult): Either<Exception, Unit> {
|
||||
return try {
|
||||
repository.emitResult(type, qrCode)
|
||||
repository.emitResult(qrCode)
|
||||
Unit.right()
|
||||
} catch (e: Exception) {
|
||||
e.left()
|
||||
|
|
|
|||
|
|
@ -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<Exception, Flow<String>> {
|
||||
return try {
|
||||
repository.subscribeToScanningResults(type).map { it.qrCode }.right()
|
||||
} catch (e: Exception) {
|
||||
e.left()
|
||||
}
|
||||
}
|
||||
|
||||
fun listen(type: SourceType): Either<Exception, Flow<RawQrResult>> {
|
||||
return try {
|
||||
repository.subscribeToScanningResults(type).right()
|
||||
} catch (e: Exception) {
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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<Network>,
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<WcConnectionsBottomSheetConfig> = SlotNavigation()
|
||||
|
||||
init {
|
||||
analytics.send(WcAnalyticEvents.ScreenOpened)
|
||||
listenQrUpdates()
|
||||
listenWcSessions()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<WcTransactionModelParams>()
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue