Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-23 11:16:28 +03:00
commit ca49abf2b1
892 changed files with 17979 additions and 6555 deletions

View file

@ -30,6 +30,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.utils)
implementation(projects.core.analytics)
/* DI */
implementation(deps.hilt.core)

View file

@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.DefaultWalletConnectRepository
import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase
import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork
@ -42,6 +43,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions")
internal object WalletConnectDataModule {
@Provides
@ -86,6 +88,7 @@ internal object WalletConnectDataModule {
dispatchers: CoroutineDispatcherProvider,
legacyStore: WalletConnectSessionsRepository,
getWallets: GetWalletsUseCase,
associateNetworks: AssociateNetworksDelegate,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcSessionsManager(
@ -93,6 +96,7 @@ internal object WalletConnectDataModule {
dispatchers = dispatchers,
legacyStore = legacyStore,
getWallets = getWallets,
associateNetworks = associateNetworks,
scope = scope,
)
}
@ -121,21 +125,23 @@ internal object WalletConnectDataModule {
@Singleton
fun wcEthNetwork(
@SdkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
sessionsManager: WcSessionsManager,
factories: WcEthNetwork.Factories,
namespaceConverter: WcEthNetwork.NamespaceConverter,
walletManagersFacade: WalletManagersFacade,
): WcEthNetwork = WcEthNetwork(
moshi = moshi,
excludedBlockchains = excludedBlockchains,
namespaceConverter = namespaceConverter,
sessionsManager = sessionsManager,
factories = factories,
walletManagersFacade = walletManagersFacade,
)
@Provides
@Singleton
fun wcSolanaNetwork(
@SdkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
namespaceConverter: WcSolanaNetwork.NamespaceConverter,
sessionsManager: WcSessionsManager,
factories: WcSolanaNetwork.Factories,
walletManager: UserWalletManager,
@ -143,28 +149,28 @@ internal object WalletConnectDataModule {
moshi = moshi,
sessionsManager = sessionsManager,
factories = factories,
excludedBlockchains = excludedBlockchains,
namespaceConverter = namespaceConverter,
walletManager = walletManager,
)
@Provides
@Singleton
fun caipNamespaceDelegate(
diHelperBox: DiHelperBox,
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
walletManagersFacade: WalletManagersFacade,
): CaipNamespaceDelegate = CaipNamespaceDelegate(
namespaceConverters = diHelperBox.converters,
namespaceConverters = namespaceConverters,
walletManagersFacade = walletManagersFacade,
)
@Provides
@Singleton
fun associateNetworksDelegate(
diHelperBox: DiHelperBox,
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
getWallets: GetWalletsUseCase,
currenciesRepository: CurrenciesRepository,
): AssociateNetworksDelegate = AssociateNetworksDelegate(
namespaceConverters = diHelperBox.converters,
namespaceConverters = namespaceConverters,
getWallets = getWallets,
currenciesRepository = currenciesRepository,
)
@ -176,10 +182,16 @@ internal object WalletConnectDataModule {
ethNetwork,
solanaNetwork,
),
converters = setOf(
ethNetwork,
solanaNetwork,
),
)
@Provides
@Singleton
fun namespaceConverters(
ethNamespaceConverter: WcEthNetwork.NamespaceConverter,
solanaNamespaceConverter: WcSolanaNetwork.NamespaceConverter,
): Set<@JvmSuppressWildcards WcNamespaceConverter> = setOf(
ethNamespaceConverter,
solanaNamespaceConverter,
)
@Provides
@ -190,12 +202,28 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun providesWcDisconnectUseCase(sessionsManager: WcSessionsManager): WcDisconnectUseCase {
return WcDisconnectUseCase(sessionsManager)
fun wcEthNetworkNamespaceConverter(excludedBlockchains: ExcludedBlockchains): WcEthNetwork.NamespaceConverter {
return WcEthNetwork.NamespaceConverter(excludedBlockchains)
}
@Provides
@Singleton
fun wcSolanaNetworkNamespaceConverter(
excludedBlockchains: ExcludedBlockchains,
): WcSolanaNetwork.NamespaceConverter {
return WcSolanaNetwork.NamespaceConverter(excludedBlockchains)
}
@Provides
@Singleton
fun providesWcDisconnectUseCase(
sessionsManager: WcSessionsManager,
analytics: AnalyticsEventHandler,
): WcDisconnectUseCase {
return WcDisconnectUseCase(sessionsManager, analytics)
}
internal class DiHelperBox(
val converters: Set<WcNamespaceConverter>,
val handlers: Set<WcRequestToUseCaseConverter>,
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
import com.tangem.data.walletconnect.request.DefaultWcRequestService
import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import timber.log.Timber
@ -56,6 +57,7 @@ internal class DefaultWcInitializeUseCase(
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
wcSdkObservers.forEach { it.onWcSdkInit() }
Timber.tag(WC_TAG).i("onWcSdkInit")
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")
@ -68,26 +70,32 @@ internal class DefaultWcInitializeUseCase(
get() = super.onSessionAuthenticate
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
Timber.tag(WC_TAG).i("sdk callback onConnectionStateChange isAvailable=${state.isAvailable}")
wcSdkObservers.forEach { it.onConnectionStateChange(state) }
}
override fun onError(error: Wallet.Model.Error) {
Timber.tag(WC_TAG).e(error.throwable, "sdk callback onError")
wcSdkObservers.forEach { it.onError(error) }
}
override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {
Timber.tag(WC_TAG).i("sdk callback onProposalExpired $proposal")
wcSdkObservers.forEach { it.onProposalExpired(proposal) }
}
override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) {
Timber.tag(WC_TAG).i("sdk callback onRequestExpired $request")
wcSdkObservers.forEach { it.onRequestExpired(request) }
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
Timber.tag(WC_TAG).i("sdk callback onSessionDelete $sessionDelete")
wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) }
}
override fun onSessionExtend(session: Wallet.Model.Session) {
Timber.tag(WC_TAG).i("sdk callback onSessionExtend $session")
wcSdkObservers.forEach { it.onSessionExtend(session) }
}
@ -95,6 +103,7 @@ internal class DefaultWcInitializeUseCase(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
Timber.tag(WC_TAG).i("sdk callback onSessionProposal $sessionProposal")
wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) }
}
@ -102,14 +111,17 @@ internal class DefaultWcInitializeUseCase(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
Timber.tag(WC_TAG).i("sdk callback onSessionRequest $sessionRequest")
wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) }
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
Timber.tag(WC_TAG).i("sdk callback onSessionSettleResponse $settleSessionResponse")
wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) }
}
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
Timber.tag(WC_TAG).i("sdk callback onSessionUpdateResponse $sessionUpdateResponse")
wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) }
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.data.walletconnect.network.ethereum
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.walletconnect.model.WcEthTransactionParams
import com.tangem.domain.walletconnect.usecase.method.WcEthTransaction
import com.tangem.utils.converter.Converter
internal class EthTransactionParamsConverter(
private val context: WcMethodUseCaseContext,
) : Converter<WcEthTransactionParams, WcEthTransaction?> {
override fun convert(value: WcEthTransactionParams): WcEthTransaction? {
val dAppFee = WcEthTxHelper.getDAppFee(context.network, value)
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee,
network = context.network,
txParams = value,
)
transactionData ?: return null
return WcEthTransaction(
dAppFee = dAppFee,
transactionData = transactionData,
)
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.Either
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class WcEthAddNetworkUseCase @AssistedInject constructor(
private val respondService: WcRespondService,
@Assisted val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.AddEthereumChain,
) : WcAddNetworkUseCase {
override val session: WcSession
get() = context.session
override val rawSdkRequest: WcSdkSessionRequest
get() = context.rawSdkRequest
override val network: Network
get() = context.network
override val walletAddress: String
get() = context.accountAddress
override suspend fun approve(): Either<Throwable, Unit> {
return respondService.respond(rawSdkRequest, "")
}
override fun reject() {
respondService.rejectRequestNonBlock(rawSdkRequest)
}
@AssistedFactory
interface Factory {
fun create(context: WcMethodUseCaseContext, method: WcEthMethod.AddEthereumChain): WcEthAddNetworkUseCase
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.blockchain.extensions.isAscii
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper.prepareToSendMessageData
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
@ -29,8 +30,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
@Suppress("LongParameterList")
internal class WcEthMessageSignUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.MessageSign,
private val walletManagersFacade: WalletManagersFacade,
@ -79,15 +82,12 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor(
object LegacySdkHelper {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
internal fun prepareToSendMessageData(
signedHash: ByteArray,
hashToSign: ByteArray,
walletManager: WalletManager,
): String = UnmarshalHelper.unmarshalSignatureExtended(
signature = signedHash,
hash = hashToSign,
publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().formatHex().lowercase() // use lowercase because some dapps cant handle UPPERCASE
fun prepareToSendMessageData(signedHash: ByteArray, hashToSign: ByteArray, walletManager: WalletManager): String =
UnmarshalHelper.unmarshalSignatureExtended(
signature = signedHash,
hash = hashToSign,
publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().formatHex().lowercase() // use lowercase because some dapps cant handle UPPERCASE
fun createMessageData(message: String): ByteArray {
val messageData = try {

View file

@ -9,25 +9,22 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcEthMethodName
import com.tangem.domain.walletconnect.model.WcEthSignTypedDataParams
import com.tangem.domain.walletconnect.model.WcEthTransactionParams
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import jakarta.inject.Inject
internal class WcEthNetwork(
private val moshi: Moshi,
private val excludedBlockchains: ExcludedBlockchains,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
) : WcRequestToUseCaseConverter, WcNamespaceConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
private val namespaceConverter: NamespaceConverter,
private val walletManagersFacade: WalletManagersFacade,
) : WcRequestToUseCaseConverter {
override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? {
val methodKey = request.request.method
@ -35,16 +32,19 @@ internal class WcEthNetwork(
return name
}
@Suppress("CyclomaticComplexMethod")
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val name = toWcMethodName(request) ?: return null
val method: WcEthMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val method: WcEthMethod = name.toMethod(request, session.wallet) ?: return null
val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val walletManagerAddress = walletManagersFacade.getDefaultAddress(session.wallet.walletId, network).orEmpty()
val accountAddress = when (method) {
is WcEthMethod.MessageSign -> method.account
is WcEthMethod.SendTransaction -> method.transaction.from
is WcEthMethod.SignTransaction -> method.transaction.from
is WcEthMethod.SignTypedData -> method.account
is WcEthMethod.AddEthereumChain -> walletManagerAddress
}
val context = WcMethodUseCaseContext(
session = session,
@ -57,10 +57,11 @@ internal class WcEthNetwork(
is WcEthMethod.SendTransaction -> factories.sendTransaction.create(context, method)
is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method)
is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method)
}
}
private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest): WcEthMethod? {
private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest, wallet: UserWallet): WcEthMethod? {
val rawParams = request.request.params
return when (this) {
WcEthMethodName.EthSign,
@ -80,6 +81,13 @@ internal class WcEthNetwork(
WcEthMethod.SendTransaction(transaction = it)
}
}
WcEthMethodName.AddEthereumChain -> moshi.fromJson<List<WcEthAddChain>>(rawParams)
?.firstOrNull()
?.let {
val newNetwork = namespaceConverter
.toNetwork(it.chainId, wallet) ?: return null
WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork)
}
}
}
@ -100,24 +108,31 @@ internal class WcEthNetwork(
return WcEthMethod.SignTypedData(params = params, account = account, dataForSign = data)
}
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
internal class NamespaceConverter(
private val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.rawId)
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
}
}
internal class Factories @Inject constructor(
@ -125,5 +140,6 @@ internal class WcEthNetwork(
val signTypedData: WcEthSignTypedDataUseCase.Factory,
val sendTransaction: WcEthSendTransactionUseCase.Factory,
val signTransaction: WcEthSignTransactionUseCase.Factory,
val addNetwork: WcEthAddNetworkUseCase.Factory,
)
}

View file

@ -1,23 +1,26 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.SignCollector
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.method.WcEthTransaction
import com.tangem.domain.walletconnect.usecase.method.WcEthTransactionUseCase
import com.tangem.domain.walletconnect.usecase.method.WcMutableFee
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
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
@ -25,10 +28,12 @@ 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,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<Fee, WcEthTransaction>(),
WcEthTransactionUseCase {
) : BaseWcSignUseCase<Fee, TransactionData>(),
WcTransactionUseCase,
WcMutableFee {
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
@ -38,10 +43,8 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
accountAddress = context.accountAddress,
)
private val converter = EthTransactionParamsConverter(context)
override suspend fun SignCollector<WcEthTransaction>.onSign(state: WcSignState<WcEthTransaction>) {
val hash = sendTransaction(state.signModel.transactionData, wallet, network)
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = sendTransaction(state.signModel, wallet, network)
.onLeft { error ->
val sendError = IllegalArgumentException(error.toString()) // todo(wc) use domain error
emit(state.toResult(sendError.left()))
@ -51,13 +54,31 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(state.toResult(respondResult))
}
override suspend fun FlowCollector<TransactionData>.onMiddleAction(signModel: TransactionData, fee: Fee) {
val uncompiled = signModel.requireUncompiled()
val newState = uncompiled.copy(fee = fee)
emit(newState)
}
override suspend fun dAppFee(): Fee.Ethereum.Legacy? {
val dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
return dAppFee
}
override fun updateFee(fee: Fee) {
middleAction(fee)
}
override fun invoke(): Flow<WcSignState<WcEthTransaction>> = flow {
val ethTransaction = converter.convert(method.transaction) ?: return@flow
emitAll(delegate.invoke(ethTransaction))
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee(),
network = context.network,
txParams = method.transaction,
) ?: return@flow
emitAll(delegate.invoke(transactionData))
}
@AssistedFactory

View file

@ -1,20 +1,22 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
import com.tangem.common.extensions.toHexString
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.SignCollector
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.method.WcEthTransaction
import com.tangem.domain.walletconnect.usecase.method.WcEthTransactionUseCase
import com.tangem.domain.walletconnect.usecase.method.WcMutableFee
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -25,12 +27,14 @@ import kotlinx.coroutines.flow.flow
internal class WcEthSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SignTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<Fee, WcEthTransaction>(),
WcEthTransactionUseCase {
) : BaseWcSignUseCase<Fee, TransactionData>(),
WcTransactionUseCase,
WcMutableFee {
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
@ -40,10 +44,8 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
accountAddress = context.accountAddress,
)
private val converter = EthTransactionParamsConverter(context)
override suspend fun SignCollector<WcEthTransaction>.onSign(state: WcSignState<WcEthTransaction>) {
val hash = prepareForSend(state.signModel.transactionData, wallet, network)
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = prepareForSend(state.signModel, wallet, network)
.map { it.toHexString().formatHex() }
.onLeft { error ->
emit(state.toResult(error.left()))
@ -54,9 +56,9 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
emit(state.toResult(respondResult))
}
override suspend fun FlowCollector<WcEthTransaction>.onMiddleAction(signModel: WcEthTransaction, fee: Fee) {
val newState = signModel
.copy(transactionData = signModel.transactionData.copy(fee = fee))
override suspend fun FlowCollector<TransactionData>.onMiddleAction(signModel: TransactionData, fee: Fee) {
val uncompiled = signModel.requireUncompiled()
val newState = uncompiled.copy(fee = fee)
emit(newState)
}
@ -64,9 +66,21 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
middleAction(fee)
}
override fun invoke(): Flow<WcSignState<WcEthTransaction>> = flow {
val ethTransaction = converter.convert(method.transaction) ?: return@flow
emitAll(delegate.invoke(ethTransaction))
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
val transactionData = WcEthTxHelper.createTransactionData(
dAppFee = dAppFee(),
network = context.network,
txParams = method.transaction,
) ?: return@flow
emitAll(delegate.invoke(transactionData))
}
override suspend fun dAppFee(): Fee.Ethereum.Legacy? {
val dAppFee = WcEthTxHelper.getDAppFee(
network = context.network,
txParams = method.transaction,
)
return dAppFee
}
@AssistedFactory

View file

@ -2,6 +2,7 @@ package com.tangem.data.walletconnect.network.ethereum
import arrow.core.left
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper.prepareToSendMessageData
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
@ -21,8 +22,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
@Suppress("LongParameterList")
internal class WcEthSignTypedDataUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SignTypedData,
private val walletManagersFacade: WalletManagersFacade,

View file

@ -9,7 +9,7 @@ import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.common.extensions.hexToBytes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcEthTransactionParams
import java.math.BigDecimal
@ -20,7 +20,7 @@ internal object WcEthTxHelper {
val gasLimit = txParams.gas?.hexToBigDecimal() ?: return null
val gasPrice = txParams.gasPrice?.hexToBigDecimal() ?: return null
val blockchain = Blockchain.fromId(network.id.value)
val blockchain = Blockchain.fromId(network.rawId)
var feeDecimal = (gasLimit * gasPrice)
.movePointLeft(blockchain.decimals())
@ -38,7 +38,7 @@ internal object WcEthTxHelper {
txParams: WcEthTransactionParams,
): TransactionData.Uncompiled? {
val destinationAddress = txParams.to ?: return null
val blockchain = Blockchain.fromId(network.id.value)
val blockchain = Blockchain.fromId(network.rawId)
val value = (txParams.value ?: "0")
.hexToBigDecimal()
.movePointLeft(blockchain.decimals())

View file

@ -4,6 +4,7 @@ import arrow.core.left
import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchain.extensions.encodeBase58
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.SignCollector
@ -25,6 +26,7 @@ internal class WcSolanaMessageSignUseCase @AssistedInject constructor(
@Assisted override val method: WcSolanaMethod.SignMessage,
private val signUseCase: SignUseCase,
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
) : BaseWcSignUseCase<Nothing, WcMessageSignUseCase.SignModel>(),
WcMessageSignUseCase {

View file

@ -11,7 +11,7 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -26,11 +26,9 @@ internal class WcSolanaNetwork(
private val moshi: Moshi,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
private val excludedBlockchains: ExcludedBlockchains,
private val namespaceConverter: NamespaceConverter,
private val walletManager: UserWalletManager,
) : WcNamespaceConverter, WcRequestToUseCaseConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
) : WcRequestToUseCaseConverter {
override fun toWcMethodName(request: WcSdkSessionRequest): WcSolanaMethodName? {
val methodKey = request.request.method
@ -42,7 +40,7 @@ internal class WcSolanaNetwork(
val name = toWcMethodName(request) ?: return null
val method: WcSolanaMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val accountAddress = getAccountAddress(network)
val context = WcMethodUseCaseContext(
session = session,
@ -59,38 +57,45 @@ internal class WcSolanaNetwork(
private suspend fun getAccountAddress(network: Network): String? {
return try {
walletManager.getWalletAddress(network.id.value, network.derivationPath.value)
walletManager.getWalletAddress(network.rawId, network.derivationPath.value)
} catch (exception: Exception) {
Timber.e(exception)
null
}
}
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
internal class NamespaceConverter @Inject constructor(
private val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID
else -> null
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.rawId)
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID
else -> null
}
chainId ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId,
)
}
chainId ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId,
)
}
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? {
@ -114,8 +119,8 @@ internal class WcSolanaNetwork(
internal class Factories @Inject constructor(
val messageSign: WcSolanaMessageSignUseCase.Factory,
val signTransaction: DefaultWcSolanaSignTransactionUseCase.Factory,
val signAllTransaction: DefaultWcSolanaSignAllTransactionUseCase.Factory,
val signTransaction: WcSolanaSignTransactionUseCase.Factory,
val signAllTransaction: WcSolanaSignAllTransactionUseCase.Factory,
)
companion object {

View file

@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.encodeBase64
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.SignCollector
@ -11,8 +12,8 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcListTransactionUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.solana.WcSolanaSignAllTransactionUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -21,14 +22,15 @@ import okio.ByteString.Companion.decodeBase64
import org.json.JSONArray
import org.json.JSONObject
internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject constructor(
internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignAllTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<Nothing, List<TransactionData.Compiled>>(),
WcSolanaSignAllTransactionUseCase {
) : BaseWcSignUseCase<Nothing, List<TransactionData>>(),
WcListTransactionUseCase {
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
@ -38,9 +40,7 @@ internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject construc
accountAddress = context.accountAddress,
)
override suspend fun SignCollector<List<TransactionData.Compiled>>.onSign(
state: WcSignState<List<TransactionData.Compiled>>,
) {
override suspend fun SignCollector<List<TransactionData>>.onSign(state: WcSignState<List<TransactionData>>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(error.left()))
@ -52,7 +52,7 @@ internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject construc
emit(state.toResult(respondResult))
}
override fun invoke(): Flow<WcSignState<List<TransactionData.Compiled>>> {
override fun invoke(): Flow<WcSignState<List<TransactionData>>> {
val transactionData = method.transaction
.map { it.decodeBase64()?.toByteArray() ?: ByteArray(0) }
.map { TransactionData.Compiled(value = TransactionData.Compiled.Data.Bytes(it)) }
@ -83,6 +83,6 @@ internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject construc
fun create(
context: WcMethodUseCaseContext,
method: WcSolanaMethod.SignAllTransaction,
): DefaultWcSolanaSignAllTransactionUseCase
): WcSolanaSignAllTransactionUseCase
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.encodeBase58
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.SignCollector
@ -12,21 +13,22 @@ import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.solana.WcSolanaSignTransactionUseCase
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import okio.ByteString.Companion.decodeBase64
internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor(
internal class WcSolanaSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
) : BaseWcSignUseCase<Nothing, TransactionData.Compiled>(),
WcSolanaSignTransactionUseCase {
) : BaseWcSignUseCase<Nothing, TransactionData>(),
WcTransactionUseCase {
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
@ -36,7 +38,7 @@ internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor
accountAddress = context.accountAddress,
)
override suspend fun SignCollector<TransactionData.Compiled>.onSign(state: WcSignState<TransactionData.Compiled>) {
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(error.left()))
@ -48,7 +50,7 @@ internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor
emit(state.toResult(respondResult))
}
override fun invoke(): Flow<WcSignState<TransactionData.Compiled>> {
override fun invoke(): Flow<WcSignState<TransactionData>> {
val data = method.transaction.decodeBase64()?.toByteArray() ?: ByteArray(0)
val transactionData = TransactionData.Compiled(
@ -62,6 +64,6 @@ internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor
fun create(
context: WcMethodUseCaseContext,
method: WcSolanaMethod.SignTransaction,
): DefaultWcSolanaSignTransactionUseCase
): WcSolanaSignTransactionUseCase
}
}

View file

@ -1,21 +1,32 @@
package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.Wallet.Model.Namespace
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
internal class AssociateNetworksDelegate constructor(
internal class AssociateNetworksDelegate(
private val namespaceConverters: Set<WcNamespaceConverter>,
private val getWallets: GetWalletsUseCase,
private val currenciesRepository: CurrenciesRepository,
) {
suspend fun associate(wallet: UserWallet, namespaces: Map<String, Namespace.Session>): Set<Network> {
val walletNetworks = getWalletNetworks(wallet)
val namespacesSet = namespaces.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
return namespacesSet.mapNotNullTo(mutableSetOf()) { chainId ->
val wcNetwork = namespaceConverters
.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@mapNotNullTo null
walletNetworks.find { network -> wcNetwork.id == network.id }
}
}
@Throws(WcPairError.UnsupportedNetworks::class)
suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map<UserWallet, ProposalNetwork> {
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
@ -31,9 +42,7 @@ internal class AssociateNetworksDelegate constructor(
requiredNamespaces: Set<String>,
optionalNamespaces: Set<String>,
): ProposalNetwork {
val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
val walletNetworks = getWalletNetworks(wallet)
val unknownRequired = mutableSetOf<String>()
val missingRequired = mutableSetOf<Network>()
@ -74,7 +83,12 @@ internal class AssociateNetworksDelegate constructor(
)
}
private fun Map<String, Wallet.Model.Namespace.Proposal>.setOfChainId(): Set<String> =
private suspend fun getWalletNetworks(wallet: UserWallet): List<Network> =
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
private fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
private fun missingNetworkName(chainId: String): String = chainId.replaceFirstChar(Char::titlecase)

View file

@ -3,11 +3,11 @@ package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
internal class CaipNamespaceDelegate constructor(
internal class CaipNamespaceDelegate(
private val namespaceConverters: Set<WcNamespaceConverter>,
private val walletManagersFacade: WalletManagersFacade,
) {
@ -43,7 +43,7 @@ internal class CaipNamespaceDelegate constructor(
session.methods.addAll(methods)
session.events.addAll(events)
}
return result.mapValues { (namespaceKey, session) ->
return result.mapValues { (_, session) ->
Wallet.Model.Namespace.Session(
chains = session.chains.toList(),
accounts = session.accounts.toList(),

View file

@ -7,6 +7,9 @@ import arrow.core.right
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.model.*
@ -14,25 +17,23 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.models.UserWallet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.*
import timber.log.Timber
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
@Suppress("LongParameterList")
internal class DefaultWcPairUseCase @AssistedInject constructor(
private val sessionsManager: WcSessionsManager,
private val associateNetworksDelegate: AssociateNetworksDelegate,
private val caipNamespaceDelegate: CaipNamespaceDelegate,
private val sdkDelegate: WcPairSdkDelegate,
private val blockAidVerifier: BlockAidVerifier,
private val analytics: AnalyticsEventHandler,
@Assisted private val pairRequest: WcPairRequest,
) : WcPairUseCase {
@ -41,26 +42,35 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
override operator fun invoke(): Flow<WcPairState> {
val (uri: String, source: WcPairRequest.Source) = pairRequest
return flow {
Timber.tag(WC_TAG).i("start pair flow $pairRequest")
analytics.send(WcAnalyticEvents.NewPairInitiated(source))
emit(WcPairState.Loading)
val sdkSessionProposal = sdkDelegate.pair(uri)
.onLeft { emit(WcPairState.Error(it)) }
.onLeft {
Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest")
emit(WcPairState.Error(it))
}
.getOrNull() ?: return@flow
// check unsupported dApps, just local constant for now, finish if unsupported
if (sdkSessionProposal.name in unsupportedDApps) {
Timber.i("Unsupported DApp")
Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}")
val error = WcPairState.Error(WcPairError.UnsupportedDApp)
emit(error)
return@flow
}
val proposalState = buildProposalState(sdkSessionProposal)
.onLeft { emit(WcPairState.Error(it)) }
.onLeft {
analytics.send(WcAnalyticEvents.PairFailed)
emit(WcPairState.Error(it))
}
.getOrNull() ?: return@flow
emit(proposalState)
// wait first terminal action and continue WC pair flow
Timber.tag(WC_TAG).i("pair wait terminal action ${sdkSessionProposal.name}")
val terminalAction = onCallTerminalAction.receiveAsFlow().first()
val sessionForApprove: WcSessionApprove? = when (terminalAction) {
is TerminalAction.Approve -> terminalAction.sessionForApprove
@ -68,6 +78,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}
// finish flow if rejected above
if (sessionForApprove == null) {
analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession))
sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey)
return@flow
}
@ -78,22 +89,41 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
sessionForApprove = sessionForApprove,
sdkSessionProposal = sdkSessionProposal,
).map { settledSession ->
val newSession = settledSession.session.toDomain(
val newSession = WcSession(
wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(settledSession.session),
securityStatus = proposalState.dAppSession.securityStatus,
networks = sessionForApprove.network.toSet(),
)
sessionsManager.saveSession(newSession)
analytics.send(
WcAnalyticEvents.DAppConnected(
proposalState.dAppSession,
sessionForApprove,
),
)
newSession
}.onLeft {
analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.message))
Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}")
}
emit(WcPairState.Approving.Result(sessionForApprove, either))
}.onCompletion {
if (it != null) {
Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest")
} else {
Timber.tag(WC_TAG).i("Completed successfully $pairRequest")
}
}
}
override fun approve(sessionForApprove: WcSessionApprove) {
analytics.send(WcAnalyticEvents.PairButtonConnect)
onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove))
}
override fun reject() {
analytics.send(WcAnalyticEvents.ButtonCancel(WcAnalyticEvents.ButtonCancel.Type.Connection))
onCallTerminalAction.trySend(TerminalAction.Reject)
}
@ -118,9 +148,17 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
): Either<WcPairError, WcPairState.Proposal> = runCatching {
val proposalNetwork = associateNetworksDelegate.associate(sessionProposal)
val verificationInfo = blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse {
Timber.e("Failed to verify DApp: ${it.localizedMessage}")
Timber.tag(WC_TAG).e(it, "Failed to verify DApp ${sessionProposal.name}")
CheckDAppResult.FAILED_TO_VERIFY
}
val requestedNetworks = proposalNetwork
.values.map { it.available.plus(it.required) }.flatten().toSet()
analytics.send(
WcAnalyticEvents.PairRequested(
network = requestedNetworks,
verificationInfo.name,
),
)
val appMetaData = WcAppMetaData(
name = sessionProposal.name,
description = sessionProposal.description,
@ -141,14 +179,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}
},)
private fun Wallet.Model.Session.toDomain(wallet: UserWallet, securityStatus: CheckDAppResult): WcSession {
return WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = securityStatus,
)
}
private sealed interface TerminalAction {
data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction
data object Reject : TerminalAction

View file

@ -3,12 +3,14 @@ package com.tangem.data.walletconnect.request
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.domain.walletconnect.WcRequestService
import com.tangem.domain.walletconnect.model.WcMethodName
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import timber.log.Timber
internal class DefaultWcRequestService(
private val requestConverters: Set<WcRequestToUseCaseConverter>,
@ -23,8 +25,10 @@ internal class DefaultWcRequestService(
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
val sr = WcSdkSessionRequestConverter.convert(sessionRequest)
Timber.tag(WC_TAG).i("handle request $sr")
val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) }
?: WcMethodName.Unsupported(sr.request.method)
Timber.tag(WC_TAG).i("handle request name $name")
_wcRequest.trySend(name to sr)
}
}

View file

@ -5,8 +5,10 @@ import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
internal class DefaultWcRespondService : WcRespondService {
@ -22,35 +24,18 @@ internal class DefaultWcRespondService : WcRespondService {
),
),
onSuccess = {
Timber.tag(WC_TAG).i("Successful respond for request $request")
continuation.resume(Unit.right())
},
onError = {
continuation.resume(it.throwable.left())
},
)
}
override suspend fun rejectRequest(request: WcSdkSessionRequest, message: String) =
suspendCancellableCoroutine { continuation ->
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = request.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
id = request.request.id,
code = 0,
message = message,
),
),
onSuccess = {
continuation.resume(Unit.right())
},
onError = {
Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request")
continuation.resume(it.throwable.left())
},
)
}
override fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String) {
Timber.tag(WC_TAG).i("reject request $request")
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = request.topic,

View file

@ -5,6 +5,5 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
interface WcRespondService {
suspend fun respond(request: WcSdkSessionRequest, response: String): Either<Throwable, Unit>
suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either<Throwable, Unit>
fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String = "")
}

View file

@ -6,8 +6,10 @@ 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.data.walletconnect.pair.AssociateNetworksDelegate
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionDTO
@ -29,6 +31,7 @@ internal class DefaultWcSessionsManager(
private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val associateNetworks: AssociateNetworksDelegate,
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
@ -76,10 +79,12 @@ internal class DefaultWcSessionsManager(
?: return@withContext null
val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null
val wallet = storedSession.wallet
val networks = associateNetworks.associate(wallet, sdkSession.namespaces)
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = storedSession.securityStatus,
networks = networks,
)
}
@ -114,7 +119,7 @@ internal class DefaultWcSessionsManager(
return mustSaveInNewStore.isNotEmpty()
}
private fun associate(
private suspend fun associate(
inSdk: List<Wallet.Model.Session>,
inStore: Set<WcSessionDTO>,
wallets: List<UserWallet>,
@ -122,7 +127,13 @@ internal class DefaultWcSessionsManager(
val wcSessions = inStore.mapNotNull { session ->
val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null
WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), session.securityStatus)
val networks = associateNetworks.associate(wallet, sdkSession.namespaces)
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = session.securityStatus,
networks = networks,
)
}
return wcSessions
}
@ -133,6 +144,7 @@ internal class DefaultWcSessionsManager(
val haveSomeUnknown = unknownStoredSessions.isNotEmpty()
if (haveSomeUnknown) {
Timber.tag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions")
store.removeSessions(unknownStoredSessions.toSet())
}
return haveSomeUnknown

View file

@ -1,7 +1,9 @@
package com.tangem.data.walletconnect.sign
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
@ -11,11 +13,12 @@ import kotlinx.coroutines.flow.FlowCollector
internal abstract class BaseWcSignUseCase<MiddleAction, SignModel> :
WcMethodUseCase,
WcSignUseCase,
WcSignUseCase<SignModel>,
FinalActionCollector<SignModel>,
MiddleActionCollector<MiddleAction, SignModel> {
abstract val respondService: WcRespondService
abstract val analytics: AnalyticsEventHandler
abstract val context: WcMethodUseCaseContext
override val network: Network get() = context.network
@ -25,6 +28,8 @@ internal abstract class BaseWcSignUseCase<MiddleAction, SignModel> :
protected val delegate by lazy {
WcSignUseCaseDelegate(
analytics = analytics,
context = context,
finalActionCollector = this,
middleActionCollector = this,
)
@ -34,8 +39,16 @@ internal abstract class BaseWcSignUseCase<MiddleAction, SignModel> :
defaultReject()
}
override fun sign() = delegate.sign()
override fun cancel() = delegate.cancel()
override fun sign() {
analytics.send(WcAnalyticEvents.ButtonSign(context.rawSdkRequest))
delegate.sign()
}
override fun cancel() {
analytics.send(WcAnalyticEvents.ButtonCancel(WcAnalyticEvents.ButtonCancel.Type.Sign))
delegate.cancel()
}
protected fun middleAction(action: MiddleAction) = delegate.middleAction(action)
protected fun defaultReject() {

View file

@ -1,9 +1,11 @@
package com.tangem.data.walletconnect.sign
import arrow.core.left
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import kotlinx.coroutines.Job
@ -13,6 +15,8 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
private val analytics: AnalyticsEventHandler,
private val context: WcMethodUseCaseContext,
private val finalActionCollector: FinalActionCollector<SignModel>,
private val middleActionCollector: MiddleActionCollector<MiddleAction, SignModel>,
) : FinalActionCollector<SignModel> by finalActionCollector,
@ -35,6 +39,13 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
operator fun invoke(initModel: SignModel) = channelFlow {
val state = MutableStateFlow(WcSignState(initModel, WcSignStep.PreSign))
analytics.send(
WcAnalyticEvents.SignatureRequestReceived(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
),
)
state
.onEach { newState -> channel.send(newState) }
@ -54,6 +65,27 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
val errorResult = state.value.toResult(exception.left())
state.update { errorResult }
}
.onEach { state ->
val step = state.domainStep as? WcSignStep.Result ?: return@onEach
val event = step.result.fold(
ifLeft = {
WcAnalyticEvents.SignatureRequestFailed(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
it.message.orEmpty(),
)
},
ifRight = {
WcAnalyticEvents.SignatureRequestHandled(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
)
},
)
analytics.send(event)
}
var signJob: Job? = null

View file

@ -4,7 +4,7 @@ import com.domain.blockaid.models.transaction.*
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcSession

View file

@ -5,7 +5,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWallet
internal interface WcNamespaceConverter {

View file

@ -3,6 +3,8 @@ package com.tangem.data.walletconnect.utils
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
const val WC_TAG = "Wallet Connect"
internal interface WcSdkObserver : WalletKit.WalletDelegate {
override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)?

View file

@ -8,6 +8,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.reown.walletkit.client.Wallet
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate
import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
@ -20,6 +21,7 @@ import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.wallets.models.UserWalletId
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.mockk
@ -33,6 +35,7 @@ internal class DefaultWcPairUseCaseTest {
private val sessionsManager: WcSessionsManager = mockk<WcSessionsManager>()
private val associateNetworksDelegate: AssociateNetworksDelegate = mockk<AssociateNetworksDelegate>()
private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk<CaipNamespaceDelegate>()
private val analytics: AnalyticsEventHandler = mockk<AnalyticsEventHandler>(relaxed = true)
private val sdkDelegate: WcPairSdkDelegate = mockk<WcPairSdkDelegate>()
private val blockAidVerifier: BlockAidVerifier = mockk<BlockAidVerifier>()
@ -91,6 +94,7 @@ internal class DefaultWcPairUseCaseTest {
wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = CheckDAppResult.SAFE,
networks = setOf(),
)
private fun useCaseFactory() = DefaultWcPairUseCase(
@ -99,7 +103,8 @@ internal class DefaultWcPairUseCaseTest {
caipNamespaceDelegate = caipNamespaceDelegate,
sdkDelegate = sdkDelegate,
blockAidVerifier = blockAidVerifier,
pairRequest = WcPairRequest(url, source),
analytics = analytics,
pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source),
)
@Before

View file

@ -3,14 +3,20 @@ package com.tangem.domain.walletconnect
import app.cash.turbine.test
import arrow.core.left
import arrow.core.right
import com.tangem.data.walletconnect.sign.FinalActionCollector
import com.tangem.data.walletconnect.sign.MiddleActionCollector
import com.tangem.data.walletconnect.sign.SignCollector
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.sign.*
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning
import com.tangem.data.walletconnect.sign.WcSignUseCaseDelegate
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
@ -25,6 +31,36 @@ internal class WcSignUseCaseDelegateTest {
private var finalActionCollector: FinalActionCollector<TestSignModel> =
object : FinalActionCollector<TestSignModel> {}
private val initSignModel = TestSignModel()
private val analytics: AnalyticsEventHandler = mockk<AnalyticsEventHandler>(relaxed = true)
private val rawRequestMock = WcSdkSessionRequest(
topic = "",
chainId = "",
request = WcSdkSessionRequest.JSONRPCRequest(
id = 0L,
method = "",
params = "",
),
)
private val context: WcMethodUseCaseContext = WcMethodUseCaseContext(
network = MockCryptoCurrencyFactory().ethereum.network,
accountAddress = "",
rawSdkRequest = rawRequestMock,
session = WcSession(
wallet = MockUserWalletFactory.create(),
networks = setOf(),
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
sdkModel = WcSdkSession(
topic = "",
appMetaData = WcAppMetaData(
name = "",
description = "",
url = "",
icons = listOf(),
redirect = "",
),
),
),
)
private val initState = WcSignState(initSignModel, WcSignStep.PreSign)
private val signing = initState.toSigning()
@ -46,6 +82,16 @@ internal class WcSignUseCaseDelegateTest {
emit(state.toResult(testException.left()))
}
private fun createUseCaseDelegate(
middleActionCollector: MiddleActionCollector<TestMiddleAction, TestSignModel>,
finalActionCollector: FinalActionCollector<TestSignModel>,
) = WcSignUseCaseDelegate(
analytics = analytics,
context = context,
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@Before
fun setup() {
middleActionCollector = object : MiddleActionCollector<TestMiddleAction, TestSignModel> {}
@ -59,7 +105,7 @@ internal class WcSignUseCaseDelegateTest {
successSign(state)
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -77,7 +123,7 @@ internal class WcSignUseCaseDelegateTest {
successSign(state)
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -99,7 +145,7 @@ internal class WcSignUseCaseDelegateTest {
failedSign(state)
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -123,7 +169,7 @@ internal class WcSignUseCaseDelegateTest {
throw exception
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -145,7 +191,7 @@ internal class WcSignUseCaseDelegateTest {
emit(result)
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -176,7 +222,7 @@ internal class WcSignUseCaseDelegateTest {
emit(expectedSignResult)
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -229,7 +275,7 @@ internal class WcSignUseCaseDelegateTest {
emit(signModel.copy(testStr = middleAction.newTestStr))
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)
@ -276,7 +322,7 @@ internal class WcSignUseCaseDelegateTest {
delay(4)
}
}
val useCase = WcSignUseCaseDelegate(
val useCase = createUseCaseDelegate(
finalActionCollector = finalActionCollector,
middleActionCollector = middleActionCollector,
)