From b38ebb399538b797b641c8daeffc360aebd81b89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 19:48:06 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../di/domain/DynamicAddressesDomainModule.kt | 8 +-- .../tangemTech/models/UserTokensResponse.kt | 4 +- .../DefaultDynamicAddressesRepository.kt | 16 ++++- .../DynamicAddressesInitializer.kt | 42 +++++++++++++ data/networks/build.gradle.kts | 1 + .../fetcher/CommonNetworkStatusFetcher.kt | 2 + .../multi/DefaultMultiNetworkStatusFetcher.kt | 15 ++++- .../DefaultMultiNetworkStatusFetcherTest.kt | 21 ++++++- .../DefaultWalletManagersFacade.kt | 17 +++++- .../DisableDynamicAddressesUseCase.kt | 2 +- .../DynamicAddressesSupportedBlockchains.kt | 23 +++---- .../dynamicaddresses/GetDerivedXpubUseCase.kt | 60 +++++++++++++++++++ .../dynamicaddresses/IsXpubDerivedUseCase.kt | 37 ------------ .../repository/DynamicAddressesRepository.kt | 2 +- .../walletmanager/WalletManagersFacade.kt | 2 + .../model/DynamicAddressesDelegate.kt | 15 +++-- 16 files changed, 196 insertions(+), 71 deletions(-) create mode 100644 data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt delete mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt index 3b9ec07528..0ab55063f9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt @@ -5,7 +5,7 @@ import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.GetDynamicAddressesStatusUseCase import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase -import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository @@ -69,10 +69,10 @@ internal object DynamicAddressesDomainModule { @Provides @Singleton - fun provideIsXpubDerivedUseCase( + fun provideGetDerivedXpubUseCase( walletManagersFacade: WalletManagersFacade, derivationsRepository: DerivationsRepository, - ): IsXpubDerivedUseCase { - return IsXpubDerivedUseCase(walletManagersFacade, derivationsRepository) + ): GetDerivedXpubUseCase { + return GetDerivedXpubUseCase(walletManagersFacade, derivationsRepository) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 4fe788204a..a4dccdfd90 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -42,7 +42,8 @@ data class UserTokensResponse( return otherToken.contractAddress == this.contractAddress && otherToken.networkId == this.networkId && otherToken.derivationPath == this.derivationPath && - otherToken.decimals == this.decimals + otherToken.decimals == this.decimals && + otherToken.dynamicAddressesEnabled == this.dynamicAddressesEnabled } override fun hashCode(): Int = calculateHashCode( @@ -50,6 +51,7 @@ data class UserTokensResponse( networkId.hashCode(), derivationPath.hashCode(), decimals.hashCode(), + dynamicAddressesEnabled.hashCode(), ) } diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt index c2c6203803..ce82539915 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt @@ -49,7 +49,12 @@ internal class DefaultDynamicAddressesRepository( } updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } - .onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) } + .onFailure { throwable -> + TangemLogger.e( + messageString = "Failed to sync tokens after dynamic addresses enable for $userWalletId", + throwable = throwable, + ) + } } } @@ -61,7 +66,12 @@ internal class DefaultDynamicAddressesRepository( } updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } - .onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) } + .onFailure { throwable -> + TangemLogger.e( + messageString = "Failed to sync tokens after dynamic addresses disable for $userWalletId", + throwable = throwable, + ) + } } } @@ -140,7 +150,7 @@ 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) + // Check if WalletManager is already in XPUB mode (dynamic addresses was previously enabled on this device) return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network) != null } diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt new file mode 100644 index 0000000000..6eae2bf3b5 --- /dev/null +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt @@ -0,0 +1,42 @@ +package com.tangem.data.dynamicaddresses + +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Provides XPUB strings for networks that need dynamic addresses restore (ENABLED_REQUIRES_SETUP). + * Uses only already-derived keys — no card scan triggered. + */ +@Singleton +class DynamicAddressesInitializer @Inject constructor( + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, + private val getDerivedXpubUseCase: GetDerivedXpubUseCase, +) { + + suspend fun getXpubs(userWalletId: UserWalletId, networks: Set): Map { + if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap() + + val result = mutableMapOf() + for (network in networks) { + val status = dynamicAddressesRepository.getStatus(userWalletId, network).firstOrNull() + if (status != DynamicAddressesStatus.ENABLED_REQUIRES_SETUP) continue + + val xpub = getDerivedXpubUseCase(userWalletId, network) + if (xpub != null) { + result[network] = xpub + } else { + TangemLogger.w("Dynamic addresses enabled but XPUB not available for ${network.id}") + } + } + return result + } +} \ No newline at end of file diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index d90e4e2a52..67c7f4eb40 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { // region Project - Data implementation(projects.data.common) + implementation(projects.data.dynamicAddresses) // endregion // region Project - Domain diff --git a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt index 38625e0f4a..17293dccca 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt @@ -43,6 +43,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor( userWalletId: UserWalletId, network: Network, networkCurrencies: Set, + xpub: String? = null, ): Either { return Either.catchOn(dispatchers.default) { val result = withContext(dispatchers.io) { @@ -52,6 +53,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor( extraTokens = networkCurrencies .filterIsInstance() .toSet(), + xpub = xpub, ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index e429c134da..1f3e0860a0 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -2,16 +2,18 @@ package com.tangem.data.networks.multi import arrow.core.raise.catch import arrow.core.raise.ensure -import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer +import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -27,11 +29,11 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ -@Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( private val networksStatusesStore: NetworksStatusesStore, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, + private val dynamicAddressesInitializer: DynamicAddressesInitializer, private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusFetcher { @@ -50,6 +52,14 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( }, ) + val xpubByNetwork = catch( + block = { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) }, + catch = { error -> + TangemLogger.e("Failed to build XPUBs for restore", error) + emptyMap() + }, + ) + val result = coroutineScope { params.networks .map { network -> @@ -58,6 +68,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( userWalletId = params.userWalletId, network = network, networkCurrencies = networksCurrencies[network].orEmpty().toSet(), + xpub = xpubByNetwork[network], ) } } diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index f02bcd3cd3..8a1a396ed2 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -3,6 +3,7 @@ package com.tangem.data.networks.multi import arrow.core.Either import arrow.core.left import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStore @@ -27,17 +28,21 @@ internal class DefaultMultiNetworkStatusFetcherTest { private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true) private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() + private val dynamicAddressesInitializer: DynamicAddressesInitializer = mockk() private val fetcher = DefaultMultiNetworkStatusFetcher( networksStatusesStore = networksStatusesStore, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, commonNetworkStatusFetcher = commonNetworkStatusFetcher, + dynamicAddressesInitializer = dynamicAddressesInitializer, dispatchers = TestingCoroutineDispatcherProvider(), ) @BeforeEach fun resetMocks() { - clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher) + clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher, dynamicAddressesInitializer) + // No dynamic addresses restore by default + coEvery { dynamicAddressesInitializer.getXpubs(any(), any()) } returns emptyMap() } @Test @@ -62,6 +67,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) } returns ethereumFetcherResult @@ -70,6 +76,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } returns cardanoFetcherResult @@ -87,11 +94,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } @@ -122,6 +131,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) } returns ethereumFetcherResult @@ -130,6 +140,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } returns cardanoFetcherResult @@ -147,11 +158,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } @@ -182,6 +195,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) } returns ethereumFetcherResult @@ -190,6 +204,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } returns cardanoFetcherResult @@ -207,11 +222,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } @@ -245,7 +262,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } - coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) } + coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any(), any()) } } private companion object { diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index d86119e490..c23bb4c58e 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -59,7 +59,7 @@ import java.util.EnumSet import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject -@Suppress("LargeClass", "TooManyFunctions") +@Suppress("LargeClass", "TooManyFunctions", "LongParameterList") internal class DefaultWalletManagersFacade @Inject constructor( private val walletManagersStore: WalletManagersStore, private val userWalletsListRepository: UserWalletsListRepository, @@ -85,6 +85,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( userWalletId: UserWalletId, network: Network, extraTokens: Set, + xpub: String?, ): UpdateWalletManagerResult { val userWallet = getUserWallet(userWalletId) val blockchain = network.toBlockchain() @@ -95,6 +96,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( blockchain = blockchain, derivationPath = derivationPath, extraTokens = extraTokens, + xpub = xpub, ) } @@ -309,6 +311,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( blockchain: Blockchain, derivationPath: String?, extraTokens: Set, + xpub: String? = null, ): UpdateWalletManagerResult { if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath)) { TangemLogger.w("Derivation missed for: $blockchain") @@ -326,6 +329,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( } val isUpdated = updateWalletManagerTokensIfNeeded(walletManager, extraTokens) + if (xpub != null) restoreXpubModeIfNeeded(walletManager, xpub) return try { if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { @@ -499,6 +503,17 @@ internal class DefaultWalletManagersFacade @Inject constructor( } } + private fun restoreXpubModeIfNeeded(walletManager: WalletManager, xpub: String) { + val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return + + try { + dynamicAddressesManager.enableDynamicAddresses(xpub) + TangemLogger.i("Restored XPUB mode for ${walletManager.wallet.blockchain}") + } catch (e: Exception) { + TangemLogger.e("Failed to restore XPUB mode: ${e.message}") + } + } + // endregion Dynamic Addresses @Deprecated("Will be removed in future") diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt index 214c7ee0b7..d0bcbb4660 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt @@ -11,7 +11,7 @@ class DisableDynamicAddressesUseCase( /** * Returns true when consolidation is required before disabling (non-base balances exist), - * or false when DA was disabled immediately. + * or false when dynamic addresses was disabled immediately. */ suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either = Either.catch { diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt index 3e732ca6dc..c3df141dd3 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt @@ -1,12 +1,13 @@ package com.tangem.domain.dynamicaddresses import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId /** * List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode). * Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred). * - * Per ASMPT-005: DA is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses. + * Dynamic addresses is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses. * Only the default derivation style per blockchain is supported. */ object DynamicAddressesSupportedBlockchains { @@ -26,22 +27,22 @@ object DynamicAddressesSupportedBlockchains { Blockchain.RavencoinTestnet, ) - private val supportedNetworkIds = supported.map { it.id }.toSet() + private val supportedNetworkIds = supported.map { it.toNetworkId() }.toSet() /** * Allowed BIP purpose nodes per network ID. * BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH). */ private val allowedPurposeByNetworkId: Map = buildMap { - put(Blockchain.Bitcoin.id, BIP84_PURPOSE) - put(Blockchain.BitcoinTestnet.id, BIP84_PURPOSE) - put(Blockchain.Litecoin.id, BIP84_PURPOSE) - put(Blockchain.BitcoinCash.id, BIP44_PURPOSE) - put(Blockchain.BitcoinCashTestnet.id, BIP44_PURPOSE) - put(Blockchain.Dogecoin.id, BIP44_PURPOSE) - put(Blockchain.Dash.id, BIP44_PURPOSE) - put(Blockchain.Ravencoin.id, BIP44_PURPOSE) - put(Blockchain.RavencoinTestnet.id, BIP44_PURPOSE) + put(Blockchain.Bitcoin.toNetworkId(), BIP84_PURPOSE) + put(Blockchain.BitcoinTestnet.toNetworkId(), BIP84_PURPOSE) + put(Blockchain.Litecoin.toNetworkId(), BIP84_PURPOSE) + put(Blockchain.BitcoinCash.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.BitcoinCashTestnet.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.Dogecoin.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.Dash.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.Ravencoin.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.RavencoinTestnet.toNetworkId(), BIP44_PURPOSE) } fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt new file mode 100644 index 0000000000..4c3b3cd2bf --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.calculateRipemd160 +import com.tangem.common.extensions.calculateSha256 +import com.tangem.crypto.NetworkType +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository + +/** + * Returns the XPUB string if account-level keys are already derived (no card scan needed), + * or null if keys are not available. + */ +class GetDerivedXpubUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val derivationsRepository: DerivationsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String? { + val blockchain = network.toBlockchain() + if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return null + if (!blockchain.isBip44DerivationStyleXPUB()) return null + + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return null + val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return null + if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return null + + val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey) + val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey) + + val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT)) + val parentPath = DerivationPath(accountPath.nodes.dropLast(1)) + + val childExtKey = existingKeys[accountPath] ?: return null + val parentExtKey = existingKeys[parentPath] ?: return null + + val parentFingerprint = parentExtKey.publicKey + .calculateSha256().calculateRipemd160() + .take(PARENT_FINGERPRINT_SIZE).toByteArray() + + val net = if (blockchain.isTestnet()) NetworkType.Testnet else NetworkType.Mainnet + return ExtendedPublicKey( + publicKey = childExtKey.publicKey, + chainCode = childExtKey.chainCode, + depth = accountPath.nodes.size, + parentFingerprint = parentFingerprint, + childNumber = accountPath.nodes.last().index, + ).serialize(net) + } + + private companion object { + const val ACCOUNT_PATH_DROP_COUNT = 2 + const val PARENT_FINGERPRINT_SIZE = 4 + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt deleted file mode 100644 index 3a62af2f51..0000000000 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.domain.dynamicaddresses - -import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.common.extensions.ByteArrayKey -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository - -/** - * Checks if the account-level XPUB key is already derived (no card scan needed). - */ -class IsXpubDerivedUseCase( - private val walletManagersFacade: WalletManagersFacade, - private val derivationsRepository: DerivationsRepository, -) { - - suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean { - val blockchain = network.toBlockchain() - if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false - if (!blockchain.isBip44DerivationStyleXPUB()) return false - - val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false - val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return false - if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return false - val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT)) - - val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey) - val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey) - return existingKeys[accountPath] != null - } - - private companion object { - const val ACCOUNT_PATH_DROP_COUNT = 2 - } -} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt index 4bcb627e16..eec289800e 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt @@ -20,6 +20,6 @@ interface DynamicAddressesRepository { suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean - /** Returns true if there are custom tokens with change/index ≠ 0 that conflict with DA */ + /** Returns true if there are custom tokens with change/index ≠ 0 that conflict with dynamic addresses */ suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt index af4b9462bb..92824ca373 100644 --- a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -38,12 +38,14 @@ interface WalletManagersFacade { * @param userWalletId The ID of the user's wallet. * @param network The network. * @param extraTokens Additional tokens. + * @param xpub XPUB string to restore dynamic addresses mode if not yet active. * @return The result of updating the wallet manager. */ suspend fun update( userWalletId: UserWalletId, network: Network, extraTokens: Set, + xpub: String? = null, ): UpdateWalletManagerResult /** diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt index a6a548aa4e..63f7200f14 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt @@ -12,7 +12,7 @@ import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase -import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -44,7 +44,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( private val createConsolidationTransactionUseCase: CreateConsolidationTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, - private val isXpubDerivedUseCase: IsXpubDerivedUseCase, + private val getDerivedXpubUseCase: GetDerivedXpubUseCase, private val dynamicAddressesRepository: DynamicAddressesRepository, private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @@ -138,7 +138,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( ) } is EnableDynamicAddressesError.ServiceError -> { - TangemLogger.e("Failed to enable DA: ${error.cause.message}") + TangemLogger.e("Failed to enable dynamic addresses: ${error.cause.message}") _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( onDismissClick = dismissBottomSheet, ) @@ -201,7 +201,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( ) } .onFailure { e -> - TangemLogger.e("Failed to disable DA: ${e.message}") + TangemLogger.e("Failed to disable dynamic addresses: ${e.message}") _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( onDismissClick = dismissBottomSheet, ) @@ -277,9 +277,8 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( ) } - // TODO: Replace with actual DA documentation URL private fun onReadMoreClick() { - // Stub: open documentation about Dynamic Addresses consolidation + // TODO: Replace with actual URL } private fun onDisableClick() { @@ -319,7 +318,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( try { dynamicAddressesRepository.disable(userWalletId, network) } catch (e: Exception) { - TangemLogger.e("Failed to disable DA after consolidation: ${e.message}") + TangemLogger.e("Failed to disable dynamic addresses after consolidation: ${e.message}") } dismissBottomSheet() onDynamicAddressesStateChanged() @@ -338,7 +337,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( // region Common private suspend fun isXpubAlreadyDerived(network: Network): Boolean { - return isXpubDerivedUseCase(userWalletId, network) + return getDerivedXpubUseCase(userWalletId, network) != null } private fun isUserCancellation(error: Throwable): Boolean {