Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-08 14:09:57 +03:00
parent 52a607c1f5
commit 9944036c4d
29 changed files with 874 additions and 60 deletions

View file

@ -30,6 +30,7 @@ dependencies {
// region Project - Libs
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
implementation(tangemDeps.card.core)
// endregion
// region DI

View file

@ -1,5 +1,6 @@
package com.tangem.data.dynamicaddresses
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@ -10,6 +11,7 @@ import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
@ -31,17 +33,20 @@ internal class DefaultDynamicAddressesRepository(
.map { response ->
val token = response.findToken(network)
when {
token?.dynamicAddressesEnabled == true -> DynamicAddressesStatus.ENABLED
else -> DynamicAddressesStatus.DISABLED
token?.dynamicAddressesEnabled != true -> DynamicAddressesStatus.DISABLED
!isXpubAvailable(userWalletId, network) -> DynamicAddressesStatus.ENABLED_REQUIRES_SETUP
else -> DynamicAddressesStatus.ENABLED
}
// TODO handle ENABLED_REQUIRES_SETUP when XPUB is not derived locally
}
.flowOn(dispatchers.io)
}
override suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String) {
withContext(dispatchers.io) {
walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
val result = walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
if (result is SimpleResult.Failure) {
error("Failed to enable xpub mode for $userWalletId / ${network.id}: ${result.error}")
}
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true)
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
.onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) }
@ -50,7 +55,10 @@ internal class DefaultDynamicAddressesRepository(
override suspend fun disable(userWalletId: UserWalletId, network: Network) {
withContext(dispatchers.io) {
walletManagersFacade.disableXpubMode(userWalletId, network)
val result = walletManagersFacade.disableXpubMode(userWalletId, network)
if (result is SimpleResult.Failure) {
error("Failed to disable xpub mode for $userWalletId / ${network.id}: ${result.error}")
}
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false)
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
.onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) }
@ -70,6 +78,45 @@ internal class DefaultDynamicAddressesRepository(
return walletManagersFacade.hasDynamicAddressesNonBaseBalances(userWalletId, network)
}
override suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean {
return withContext(dispatchers.io) {
val response = walletAccountsFetcher.getSaved(userWalletId) ?: return@withContext false
val baseDerivationPath = network.derivationPath.value ?: return@withContext false
response.accounts
.flatMap { it.tokens.orEmpty() }
.any { token ->
val tokenDerivationPath = token.derivationPath ?: return@any false
token.networkId == network.backendId &&
tokenDerivationPath != baseDerivationPath &&
hasNonZeroChangeOrIndex(tokenDerivationPath, baseDerivationPath)
}
}
}
/**
* Checks if the token's derivation path has the same first 3 nodes (purpose/coin/account)
* as the base path but different change/index nodes (not both 0).
*/
private fun hasNonZeroChangeOrIndex(tokenPath: String, basePath: String): Boolean {
val tokenNodes = runCatching { DerivationPath(tokenPath).nodes }.getOrNull() ?: return false
val baseNodes = runCatching { DerivationPath(basePath).nodes }.getOrNull() ?: return false
if (tokenNodes.size < DERIVATION_NODE_COUNT || baseNodes.size < DERIVATION_NODE_COUNT) return false
// First 3 nodes must match (purpose/coin/account) by value, ignoring hardening
val isSameAccount = (0 until ACCOUNT_NODE_COUNT).all { i ->
tokenNodes[i].getIndex(includeHardened = false) == baseNodes[i].getIndex(includeHardened = false)
}
if (!isSameAccount) return false
// Check if change or index ≠ 0
val change = tokenNodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false)
val index = tokenNodes[INDEX_NODE_INDEX].getIndex(includeHardened = false)
return change != 0L || index != 0L
}
private suspend fun updateTokenDynamicAddressesFlag(
userWalletId: UserWalletId,
network: Network,
@ -92,6 +139,11 @@ internal class DefaultDynamicAddressesRepository(
}
}
private suspend fun isXpubAvailable(userWalletId: UserWalletId, network: Network): Boolean {
// Check if WalletManager is already in XPUB mode (DA was previously enabled on this device)
return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network) != null
}
private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? {
return accounts
.flatMap { it.tokens.orEmpty() }
@ -103,4 +155,11 @@ internal class DefaultDynamicAddressesRepository(
derivationPath == network.derivationPath.value &&
contractAddress == null
}
private companion object {
const val DERIVATION_NODE_COUNT = 5
const val ACCOUNT_NODE_COUNT = 3
const val CHANGE_NODE_INDEX = 3
const val INDEX_NODE_INDEX = 4
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.wallets.derivations
import arrow.core.getOrElse
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
@ -76,6 +77,23 @@ internal class DefaultDerivationsRepository @Inject constructor(
}
}
override suspend fun getExistingDerivedKeys(
userWalletId: UserWalletId,
seedKey: ByteArrayKey,
): ExtendedPublicKeysMap {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
return userWallet.getExistingDerivedKeys()[seedKey] ?: ExtendedPublicKeysMap(emptyMap())
}
private fun UserWallet.getExistingDerivedKeys(): Map<ByteArrayKey, ExtendedPublicKeysMap> {
return when (this) {
is UserWallet.Cold -> scanResponse.derivedKeys
is UserWallet.Hot -> wallets
?.associate { it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys) }
.orEmpty()
}
}
override suspend fun hasMissedDerivations(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<BackendId, String?>,