Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-13 19:48:06 +03:00
parent 27e9400b2a
commit b38ebb3995
16 changed files with 196 additions and 71 deletions

View file

@ -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
}

View file

@ -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<Network>): Map<Network, String> {
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap()
val result = mutableMapOf<Network, String>()
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
}
}

View file

@ -21,6 +21,7 @@ dependencies {
// region Project - Data
implementation(projects.data.common)
implementation(projects.data.dynamicAddresses)
// endregion
// region Project - Domain

View file

@ -43,6 +43,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
userWalletId: UserWalletId,
network: Network,
networkCurrencies: Set<CryptoCurrency>,
xpub: String? = null,
): Either<Throwable, Unit> {
return Either.catchOn(dispatchers.default) {
val result = withContext(dispatchers.io) {
@ -52,6 +53,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
extraTokens = networkCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.toSet(),
xpub = xpub,
)
}

View file

@ -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],
)
}
}

View file

@ -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 {

View file

@ -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<CryptoCurrency.Token>,
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<CryptoCurrency.Token>,
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")