Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-06 21:14:08 +03:00
parent 182aa24095
commit 94a9b132a2
23 changed files with 458 additions and 60 deletions

View file

@ -31,6 +31,7 @@ dependencies {
/** Domain */
implementation(projects.domain.account)
implementation(projects.domain.card)
implementation(projects.domain.dynamicAddresses)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)

View file

@ -11,6 +11,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.wallets.derivations.Derivations
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
@ -30,6 +31,7 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val networkFactory: NetworkFactory,
private val dispatchers: CoroutineDispatcherProvider,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
) : ColdMapDerivationsRepository {
override suspend fun derivePublicKeys(
@ -64,7 +66,7 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor(
return@withContext userWallet
}
val derivations = MissedDerivationsFinder(userWallet)
val derivations = MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
.findByNetworks(networks)
.ifEmpty {
TangemLogger.d("Nothing to derive")
@ -103,7 +105,7 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor(
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean = withContext(dispatchers.io) {
val derivations =
MissedDerivationsFinder(userWallet)
MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
networkFactory.create(

View file

@ -30,17 +30,20 @@ data class BlockchainToDerive(
*
[REDACTED_AUTHOR]
*/
class MissedDerivationsFinder private constructor(private val source: DerivationsSource) {
class MissedDerivationsFinder private constructor(
private val source: DerivationsSource,
private val isDynamicAddressesEnabled: Boolean,
) {
/**
* Secondary constructor for backward compatibility with UserWallet
*/
constructor(userWallet: UserWallet) : this(DerivationsSource.FromUserWallet(userWallet))
constructor(userWallet: UserWallet, isDynamicAddressesEnabled: Boolean) : this(
source = DerivationsSource.FromUserWallet(userWallet),
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
)
/**
* Secondary constructor for ScanResponse
*/
constructor(scanResponse: ScanResponse) : this(DerivationsSource.FromScanResponse(scanResponse))
constructor(scanResponse: ScanResponse, isDynamicAddressesEnabled: Boolean) : this(
source = DerivationsSource.FromScanResponse(scanResponse),
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
)
/** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations {
@ -115,6 +118,9 @@ class MissedDerivationsFinder private constructor(private val source: Derivation
// Extended Cardano derivation path if needed
add(getCardanoExtendedDerivationPath(derivationPath))
// Account-level and parent paths for XPUB generation (Dynamic Addresses)
addAll(getXpubDerivationPaths(derivationPath))
}
.filterNotNull()
.distinct()
@ -129,6 +135,26 @@ class MissedDerivationsFinder private constructor(private val source: Derivation
return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath)
}
/**
* Returns account-level and parent derivation paths needed for XPUB generation.
* - Account-level (e.g. m/84'/0'/0') the XPUB itself
* - Parent (e.g. m/84'/0') needed for parent fingerprint in XPUB serialization
*
* Only applicable for BIP44-style XPUB blockchains (BTC, BCH, LTC, DOGE, DASH, RVN).
*/
private fun Blockchain.getXpubDerivationPaths(derivationPath: DerivationPath): List<DerivationPath> {
if (!isDynamicAddressesEnabled) return emptyList()
if (!isBip44DerivationStyleXPUB()) return emptyList()
val nodes = derivationPath.nodes
if (nodes.size < XPUB_MIN_NODES) return emptyList()
val accountPath = DerivationPath(nodes.take(XPUB_ACCOUNT_NODE_COUNT))
val parentPath = DerivationPath(nodes.take(XPUB_PARENT_NODE_COUNT))
return listOf(accountPath, parentPath)
}
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList()
return filterNot(alreadyDerivedPaths::contains)
@ -161,4 +187,10 @@ class MissedDerivationsFinder private constructor(private val source: Derivation
}
// endregion
private companion object {
const val XPUB_MIN_NODES = 3
const val XPUB_ACCOUNT_NODE_COUNT = 3
const val XPUB_PARENT_NODE_COUNT = 2
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
@ -28,6 +29,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor(
private val networkFactory: NetworkFactory,
private val hotWalletAccessor: HotWalletAccessor,
private val dispatchers: CoroutineDispatcherProvider,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
) : HotMapDerivationsRepository {
override suspend fun derivePublicKeys(
@ -59,7 +61,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor(
userWallet: UserWallet.Hot,
networks: List<Network>,
): UserWallet.Hot = withContext(dispatchers.default) {
val derivations = MissedDerivationsFinder(userWallet)
val derivations = MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
.findByNetworks(networks)
.ifEmpty {
TangemLogger.d("Nothing to derive")
@ -103,7 +105,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor(
userWallet: UserWallet.Hot,
networksWithDerivationPath: Map<BackendId, String?>,
): Boolean = withContext(dispatchers.default) {
val derivations = MissedDerivationsFinder(userWallet)
val derivations = MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
networkFactory.create(

View file

@ -27,7 +27,7 @@ internal class MissedDerivationsFinderTest {
fun `empty derivations for empty currencies`() {
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val actual = finder.find(emptyList())
@ -39,7 +39,7 @@ internal class MissedDerivationsFinderTest {
// Bls is not supported
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf)
val actual = finder.find(currencies)
@ -61,7 +61,7 @@ internal class MissedDerivationsFinderTest {
)
}
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum
val actual = finder.find(currencies)
@ -76,7 +76,7 @@ internal class MissedDerivationsFinderTest {
fun `derivations for custom token`() {
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation
val actual = finder.find(currencies)
@ -94,7 +94,7 @@ internal class MissedDerivationsFinderTest {
fun `derivations for cardano`() {
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
val actual = finder.find(currencies)
@ -124,7 +124,7 @@ internal class MissedDerivationsFinderTest {
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
)
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf)
val actual = finder.find(currencies)
@ -139,7 +139,7 @@ internal class MissedDerivationsFinderTest {
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
)
val userWallet = MockUserWalletFactory.create(scanResponse)
val finder = MissedDerivationsFinder(userWallet)
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar
val actual = finder.find(currencies)