Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-04 18:31:47 +03:00
parent d06051845b
commit 5c74278e3d
30 changed files with 83 additions and 666 deletions

View file

@ -24,7 +24,6 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.WcRequestService
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
@ -181,14 +180,12 @@ internal object WalletConnectDataModule {
fun wcNetworksConverter(
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
singleAccountSupplier: SingleAccountSupplier,
): WcNetworksConverter = WcNetworksConverter(
namespaceConverters = namespaceConverters,
walletManagersFacade = walletManagersFacade,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
singleAccountSupplier = singleAccountSupplier,
)

View file

@ -131,12 +131,9 @@ internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor(
val caip2 = hexChainIdToCAIP2(hexChainId)
?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left()
val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet)
if (generalNetwork == null) {
return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left()
}
?: return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left()
val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(
rawChainId = caip2.raw,
wallet = wallet,
account = context.session.account,
)
if (addedNetwork == null) {

View file

@ -47,7 +47,6 @@ internal class WcEthNetwork(
?: return error("Failed to parse $name")
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(
rawChainId = chainId,
wallet = wallet,
account = account,
)
@ -74,7 +73,7 @@ internal class WcEthNetwork(
-> anyExistNetwork()
} ?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size
val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, account).size
val context = WcMethodUseCaseContext(
session = session,
rawSdkRequest = request,

View file

@ -49,7 +49,7 @@ internal class WcSolanaNetwork(
val wallet = session.wallet
val account = session.account
val chainId = request.chainId.orEmpty()
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet, account)
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, account)
suspend fun anyAddress() = anyExistNetwork()
?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() }
.orEmpty()
@ -64,7 +64,7 @@ internal class WcSolanaNetwork(
?: anyExistNetwork()
?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size
val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, account).size
val context = WcMethodUseCaseContext(
session = session,
rawSdkRequest = request,
@ -86,7 +86,7 @@ internal class WcSolanaNetwork(
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toBlockchain(chainId: CAIP2): Blockchain? {
val isMainNet = MAINNET_CHAIN_ID.any { it.lowercase() == chainId.reference.lowercase() }
val isMainNet = MAINNET_CHAIN_ID.any { it.equals(chainId.reference, ignoreCase = true) }
if (chainId.namespace != namespaceKey.key) return null
return when {
isMainNet -> Blockchain.Solana

View file

@ -8,7 +8,6 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.walletconnect.model.WcPairError
@ -23,19 +22,6 @@ internal class AssociateNetworksDelegate(
private val getWallets: GetWalletsUseCase,
) {
@Throws(WcPairError.UnsupportedBlockchains::class)
suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map<UserWallet, ProposalNetwork> {
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
val requiredNamespaces: Set<String> = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces: Set<String> = sessionProposal.optionalNamespaces.setOfChainId()
// remove duplicates
.subtract(requiredNamespaces)
return userWallets.associateWith { wallet ->
mapNetworksForPortfolio(wallet, null, requiredNamespaces, optionalNamespaces, sessionProposal)
}
}
@Throws(WcPairError.UnsupportedBlockchains::class)
suspend fun associateAccounts(sessionProposal: Wallet.Model.SessionProposal): Map<AccountId, ProposalNetwork> {
val userWallets = getWallets.invokeSync()
@ -67,13 +53,12 @@ internal class AssociateNetworksDelegate(
@Suppress("CyclomaticComplexMethod")
private suspend fun mapNetworksForPortfolio(
wallet: UserWallet,
account: Account?,
account: Account,
requiredNamespaces: Set<String>,
optionalNamespaces: Set<String>,
sessionProposal: Wallet.Model.SessionProposal,
): ProposalNetwork {
val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) }
?: getWalletNetworks(userWalletId = wallet.walletId)
val portfolioNetworks = getAccountNetworks(account.accountId)
val unknownRequired = mutableSetOf<String>()
val unknownOptional = mutableSetOf<String>()
@ -127,12 +112,6 @@ internal class AssociateNetworksDelegate(
)
}
private suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
return networksConverter.getWalletNetworks(userWalletId)
// flatten all derivation
.distinctBy { it.rawId }
}
private suspend fun getAccountNetworks(accountId: AccountId): List<Network> {
return networksConverter.getAccountNetworks(accountId)
// flatten all derivation

View file

@ -111,7 +111,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
val sessionDTO = WcSessionDTO(
topic = "",
walletId = sessionForApprove.wallet.walletId,
accountId = sessionForApprove.account?.accountId,
accountId = sessionForApprove.account.accountId,
url = sdkVerifyContext.getDappOriginUrl(),
securityStatus = proposalState.dAppSession.securityStatus,
connectingTime = connectingTime,
@ -195,7 +195,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
): Either<WcPairError, WcPairState.Proposal> = runCatching {
val proposalNetwork = associateNetworksDelegate.associate(sessionProposal)
val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal)
val verificationInfo = when {
verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE
@ -224,7 +223,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
)
val dAppSession = WcSessionProposal(
dAppMetaData = appMetaData,
proposalNetwork = proposalNetwork,
securityStatus = verificationInfo,
proposalAccountNetwork = proposalAccountNetwork,
)

View file

@ -9,7 +9,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.utils.*
import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcSession
@ -37,15 +36,12 @@ internal class DefaultWcSessionsManager(
) : WcSessionsManager, WcSdkObserver {
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
private val oneTimeMigration = MutableStateFlow(false)
override val sessions: Flow<Map<UserWallet, List<WcSession>>>
get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore }
.transform { pair ->
val (wallets, inStore) = pair
val inSdk: List<Wallet.Model.Session> = WalletKit.getListOfActiveSessions()
val someMigrate = migrateToAccountSession(inStore)
if (someMigrate) return@transform
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)
if (someRemove) return@transform // ignore emit, wait next one
@ -54,25 +50,6 @@ internal class DefaultWcSessionsManager(
.distinctUntilChanged()
.flowOn(dispatchers.io)
private suspend fun migrateToAccountSession(inStore: Set<WcSessionDTO>): Boolean {
if (oneTimeMigration.value) return false
var someMigrated = false
val updatedSessions = inStore.mapTo(mutableSetOf()) { sessionDTO ->
if (sessionDTO.accountId == null) {
someMigrated = true
val mainAccountId = AccountId.forMainCryptoPortfolio(sessionDTO.walletId)
sessionDTO.copy(accountId = mainAccountId)
} else {
sessionDTO
}
}
if (someMigrated) store.saveSessions(updatedSessions)
oneTimeMigration.value = true
return someMigrated
}
override fun onWcSdkInit() {
listenOnSessionDelete()
extendSessions()
@ -114,7 +91,7 @@ internal class DefaultWcSessionsManager(
val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession ->
val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null
val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } as? Account.CryptoPortfolio
val account = wcNetworksConverter.getAccount(storeSession.accountId) as? Account.CryptoPortfolio
?: return@mapNotNull null
val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession)
val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: ""

View file

@ -71,7 +71,7 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
network = context.network,
errorCode = error.code(),
errorMessage = errorMessage,
accountDerivation = context.session.account?.derivationIndex?.value,
accountDerivation = context.session.account.derivationIndex.value,
)
analytics.send(event)
}

View file

@ -18,8 +18,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -31,7 +29,6 @@ internal class WcNetworksConverter @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val singleAccountSupplier: SingleAccountSupplier,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) {
fun createNetwork(chainId: String, wallet: UserWallet): Network? {
@ -47,13 +44,12 @@ internal class WcNetworksConverter @Inject constructor(
val wallet = session.wallet
val allCoinNetwork = filterWalletNetworkForRequest(
rawChainId = request.chainId.orEmpty(),
wallet = session.wallet,
account = session.account,
)
val requestNetwork = allCoinNetwork.find { network ->
val address = getAddressForWC(wallet.walletId, network)
requestAddress.lowercase() == address?.lowercase()
requestAddress.equals(address, ignoreCase = true)
}
return requestNetwork
}
@ -61,13 +57,13 @@ internal class WcNetworksConverter @Inject constructor(
/**
* return network with not custom derivationPath or first custom or any
*/
suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet, account: Account?): Network? {
val networks = filterWalletNetworkForRequest(rawChainId, wallet, account)
suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, account: Account): Network? {
val networks = filterWalletNetworkForRequest(rawChainId, account)
return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull()
}
suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account?): List<String> {
return filterWalletNetworkForRequest(rawChainId, wallet, account)
suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account): List<String> {
return filterWalletNetworkForRequest(rawChainId, account)
.mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() }
}
@ -85,13 +81,8 @@ internal class WcNetworksConverter @Inject constructor(
/**
* return all exist derivation networks
*/
suspend fun filterWalletNetworkForRequest(
rawChainId: String,
wallet: UserWallet,
account: Account?,
): List<Network> {
val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) }
?: getWalletNetworks(wallet.walletId)
suspend fun filterWalletNetworkForRequest(rawChainId: String, account: Account): List<Network> {
val portfolioNetworks = getAccountNetworks(account.accountId)
val blockchain = namespaceConverters
.firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf()
@ -102,11 +93,10 @@ internal class WcNetworksConverter @Inject constructor(
suspend fun findWalletNetworks(
wallet: UserWallet,
account: Account?,
account: Account,
sdkSession: Wallet.Model.Session,
): Set<Network> {
val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) }
?: getWalletNetworks(wallet.walletId)
val portfolioNetworks = getAccountNetworks(account.accountId)
val existNetworks = sdkSession.namespaces.values
.map { it.accounts }.flatten().toSet()
.mapNotNull { CAIP10.fromRaw(it) }
@ -120,7 +110,7 @@ internal class WcNetworksConverter @Inject constructor(
// find equal address
.firstOrNull { network ->
val walletAddress = getAddressForWC(wallet.walletId, network)
walletAddress?.lowercase() == caip10.accountAddress.lowercase()
walletAddress.equals(caip10.accountAddress, ignoreCase = true)
}
}
@ -132,21 +122,12 @@ internal class WcNetworksConverter @Inject constructor(
}
suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List<Network> {
val portfolioNetworks = sessionForApprove.account?.let { getAccountNetworks(it.accountId) }
?: getWalletNetworks(sessionForApprove.wallet.walletId)
val portfolioNetworks = getAccountNetworks(sessionForApprove.account.accountId)
return sessionForApprove.network
.map { network -> portfolioNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } }
.flatten()
}
suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
)
.orEmpty()
.filterIsInstance<CryptoCurrency.Coin>().map(CryptoCurrency.Coin::network)
}
private suspend fun getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio? {
return singleAccountStatusListSupplier.getSyncOrNull(
SingleAccountStatusListProducer.Params(accountId.userWalletId),

View file

@ -15,6 +15,7 @@ import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcPairRequest
@ -73,7 +74,7 @@ internal class DefaultWcPairUseCaseTest {
get() = WcSessionApprove(
wallet = MockUserWalletFactory.create(),
network = listOf(),
account = null,
account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId),
)
private val sdkApprove: Wallet.Params.SessionApprove
@ -106,7 +107,7 @@ internal class DefaultWcPairUseCaseTest {
networks = setOf(),
connectingTime = null,
showWalletInfo = false,
account = null,
account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId),
)
private fun useCaseFactory() = DefaultWcPairUseCase(
@ -120,7 +121,6 @@ internal class DefaultWcPairUseCaseTest {
@Before
fun setup() {
coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf()
coEvery { associateNetworksDelegate.associateAccounts(sdkProposal) } returns mapOf()
coEvery {
caipNamespaceDelegate.associate(
@ -254,7 +254,6 @@ internal class DefaultWcPairUseCaseTest {
assertEquals(loading, awaitItem())
coVerifyOrder {
sdkDelegate.pair(url)
associateNetworksDelegate.associate(sdkProposal)
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
}
assert(awaitItem() is WcPairState.Proposal)

View file

@ -10,6 +10,7 @@ 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.domain.models.account.Account
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
@ -58,7 +59,7 @@ internal class WcSignUseCaseDelegateTest {
session = WcSession(
wallet = MockUserWalletFactory.create(),
networks = setOf(),
account = null,
account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId),
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
connectingTime = 0L,
sdkModel = WcSdkSession(