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

@ -0,0 +1,45 @@
package com.tangem.data.dynamicaddresses
import arrow.core.Either
import com.tangem.blockchain.common.DynamicAddressesManager
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultConsolidationRepository(
private val walletManagersFacade: WalletManagersFacade,
private val dispatchers: CoroutineDispatcherProvider,
) : ConsolidationRepository {
override suspend fun createConsolidationTransaction(
userWalletId: UserWalletId,
network: Network,
): Either<Throwable, TransactionData> = Either.catch {
withContext(dispatchers.io) {
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("WalletManager not found")
val dynamicAddressesManager = walletManager as? DynamicAddressesManager
?: error("WalletManager does not support dynamic addresses")
val fee = getNormalFee(walletManager)
when (val result = dynamicAddressesManager.createConsolidationTransaction(fee)) {
is Result.Success -> result.data
is Result.Failure -> error("Failed to create consolidation tx: ${result.error}")
}
}
}
@Suppress("UnusedParameter")
private fun getNormalFee(walletManager: WalletManager): Fee {
TODO("Fee calculation for consolidation from multiple addresses is not yet implemented")
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.dynamicaddresses
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
internal class DefaultDynamicAddressesFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : DynamicAddressesFeatureToggles {
override val isDynamicAddressesEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.DYNAMIC_ADDRESSES_ENABLED)
}

View file

@ -0,0 +1,106 @@
package com.tangem.data.dynamicaddresses
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.account.repository.AccountsCRUDRepository
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.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
internal class DefaultDynamicAddressesRepository(
private val walletAccountsFetcher: WalletAccountsFetcher,
private val walletAccountsSaver: WalletAccountsSaver,
private val accountsCRUDRepository: AccountsCRUDRepository,
private val walletManagersFacade: WalletManagersFacade,
private val dispatchers: CoroutineDispatcherProvider,
) : DynamicAddressesRepository {
override fun getStatus(userWalletId: UserWalletId, network: Network): Flow<DynamicAddressesStatus> {
return walletAccountsFetcher.get(userWalletId)
.map { response ->
val token = response.findToken(network)
when {
token?.dynamicAddressesEnabled == true -> DynamicAddressesStatus.ENABLED
else -> DynamicAddressesStatus.DISABLED
}
// 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)
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true)
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
.onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) }
}
}
override suspend fun disable(userWalletId: UserWalletId, network: Network) {
withContext(dispatchers.io) {
walletManagersFacade.disableXpubMode(userWalletId, network)
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false)
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
.onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) }
}
}
override suspend fun getReceiveAddress(userWalletId: UserWalletId, network: Network): String {
return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network)
?: error("Dynamic receive address not available for $userWalletId / ${network.id}")
}
override suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String? {
return walletManagersFacade.getDynamicAddressesLastUsedReceiveAddress(userWalletId, network)
}
override suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean {
return walletManagersFacade.hasDynamicAddressesNonBaseBalances(userWalletId, network)
}
private suspend fun updateTokenDynamicAddressesFlag(
userWalletId: UserWalletId,
network: Network,
enabled: Boolean,
) {
walletAccountsSaver.update(userWalletId) { response ->
response?.copy(
accounts = response.accounts.map { account ->
account.copy(
tokens = account.tokens?.map { token ->
if (token.matchesNetwork(network)) {
token.copy(dynamicAddressesEnabled = enabled)
} else {
token
}
},
)
},
)
}
}
private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? {
return accounts
.flatMap { it.tokens.orEmpty() }
.find { it.matchesNetwork(network) }
}
private fun UserTokensResponse.Token.matchesNetwork(network: Network): Boolean {
return networkId == network.backendId &&
derivationPath == network.derivationPath.value &&
contractAddress == null
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.data.dynamicaddresses.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.dynamicaddresses.DefaultConsolidationRepository
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.data.dynamicaddresses.DefaultDynamicAddressesFeatureToggles
import com.tangem.data.dynamicaddresses.DefaultDynamicAddressesRepository
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object DynamicAddressesDataModule {
@Provides
@Singleton
fun provideDynamicAddressesRepository(
walletAccountsFetcher: WalletAccountsFetcher,
walletAccountsSaver: WalletAccountsSaver,
accountsCRUDRepository: AccountsCRUDRepository,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): DynamicAddressesRepository {
return DefaultDynamicAddressesRepository(
walletAccountsFetcher = walletAccountsFetcher,
walletAccountsSaver = walletAccountsSaver,
accountsCRUDRepository = accountsCRUDRepository,
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideDynamicAddressesFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
): DynamicAddressesFeatureToggles {
return DefaultDynamicAddressesFeatureToggles(featureTogglesManager = featureTogglesManager)
}
@Provides
@Singleton
fun provideConsolidationRepository(
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): ConsolidationRepository {
return DefaultConsolidationRepository(
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
)
}
}