Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-03 18:17:11 +04:00
parent fbc15cbb8c
commit 563d0834c4
19 changed files with 99 additions and 1025 deletions

View file

@ -1,6 +1,5 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
@ -97,12 +96,10 @@ internal object AccountDomainModule {
fun provideIsAccountsModeEnabledUseCase(
userWalletsListRepository: UserWalletsListRepository,
accountsCRUDRepository: AccountsCRUDRepository,
accountsFeatureToggles: AccountsFeatureToggles,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(
userWalletsListRepository = userWalletsListRepository,
crudRepository = accountsCRUDRepository,
accountsFeatureToggles = accountsFeatureToggles,
)
}

View file

@ -24,10 +24,6 @@
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
"version": "5.32.0"
},
{
"name": "ACCOUNTS_FEATURE_ENABLED",
"version": "5.33.0"
},
{
"name": "APP_REDESIGN_ENABLED",
"version": "undefined"

View file

@ -4,9 +4,7 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.account.converter.AccountConverterFactoryContainer
import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher
import com.tangem.data.account.repository.AccountsExpandedDTO
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
@ -25,7 +23,6 @@ import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.account.tokens.MainAccountTokensMigration
@ -43,12 +40,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object AccountDataModule {
@Provides
@Singleton
fun provideAccountFeatureToggle(featureTogglesManager: FeatureTogglesManager): AccountsFeatureToggles {
return DefaultAccountsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
@Provides
@Singleton
fun provideAccountsCRUDRepository(

View file

@ -1,8 +1,6 @@
package com.tangem.data.account.di
import com.tangem.data.account.producer.AccountListCryptoCurrenciesProducer
import com.tangem.data.account.producer.DefaultMultiWalletCryptoCurrenciesProducer
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import dagger.Module
import dagger.Provides
@ -17,10 +15,8 @@ internal object MultiWalletCryptoCurrenciesProducerModule {
@Singleton
@Provides
fun provideMultiWalletCryptoCurrenciesProducerFactory(
accountsFeatureToggles: AccountsFeatureToggles,
defaultImpl: DefaultMultiWalletCryptoCurrenciesProducer.Factory,
accountsImpl: AccountListCryptoCurrenciesProducer.Factory,
): MultiWalletCryptoCurrenciesProducer.Factory {
return if (accountsFeatureToggles.isFeatureEnabled) accountsImpl else defaultImpl
return accountsImpl
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.data.account.featuretoggle
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
internal class DefaultAccountsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : AccountsFeatureToggles {
override val isFeatureEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "ACCOUNTS_FEATURE_ENABLED")
}

View file

@ -5,8 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
@ -23,17 +21,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency
*
* @property demoConfig demo config
* @property excludedBlockchains excluded blockchains
* @property userWalletsStore user wallets store
* @property userTokensResponseStore user tokens response store
*/
@Suppress("LongParameterList")
internal class DefaultCardCryptoCurrencyFactory(
private val demoConfig: DemoConfig,
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsListRepository: UserWalletsListRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val userTokensResponseStore: UserTokensResponseStore,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
) : CardCryptoCurrencyFactory {
@ -140,34 +134,21 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
?: return@flatMapTo emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty().filter { token ->
networks.any {
it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
}
},
userWallet = userWallet,
accountIndex = accountIndex,
)
}
} else {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
?: return@flatMapTo emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
tokens = accountDTO.tokens.orEmpty().filter { token ->
networks.any {
it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
}
},
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
accountIndex = accountIndex,
)
}
.groupBy(CryptoCurrency::network)
@ -181,28 +162,17 @@ internal class DefaultCardCryptoCurrencyFactory(
): Map<Network.RawID, List<CryptoCurrency>> {
val networkIds = rawIds.map { it.toBlockchain().toNetworkId() }
return if (accountsFeatureToggles.isFeatureEnabled) {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
?: return@flatMapTo emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
userWallet = userWallet,
accountIndex = accountIndex,
)
}
} else {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
return response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
?: return@flatMapTo emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token -> token.networkId in networkIds },
tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
accountIndex = accountIndex,
)
}
.groupBy { it.network.id.rawId }

View file

@ -8,10 +8,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncOrNull
import com.tangem.domain.models.wallet.UserWallet
@ -30,8 +27,6 @@ class UserTokensSaver(
private val dispatchers: CoroutineDispatcherProvider,
private val addressesEnricher: UserTokensResponseAddressesEnricher,
private val walletServerBinder: WalletServerBinder,
private val appsFlyerStore: AppsFlyerStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val pushTokensRetryerPool: RetryerPool,
) {
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
@ -68,22 +63,9 @@ class UserTokensSaver(
return@withContext
}
if (accountsFeatureToggles.isFeatureEnabled) {
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
} else {
val conversionData = appsFlyerStore.get()
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
walletName = userWallet.name.takeIf { it.isNotBlank() },
walletType = WalletType.from(userWallet),
refcode = conversionData?.refcode,
campaign = conversionData?.campaign,
)
pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend)
}
push(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
}
suspend fun pushWithRetryer(
@ -103,14 +85,7 @@ class UserTokensSaver(
)
}
private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) {
safeApiCall(
call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() },
onError = { onFailSend() },
)
}
private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) {
private suspend fun push(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) {
safeApiCall(
call = {
val apiResponse = tangemTechApi.saveTokens(
@ -148,13 +123,7 @@ class UserTokensSaver(
return this
.enrichByAddress(userWalletId = userWalletId)
.let { response ->
if (accountsFeatureToggles.isFeatureEnabled) {
response.enrichByAccountId(userWalletId = userWalletId)
} else {
response
}
}
.enrichByAccountId(userWalletId = userWalletId)
}
private suspend fun UserTokensResponse.enrichByAddress(userWalletId: UserWalletId): UserTokensResponse {

View file

@ -13,7 +13,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -37,18 +36,14 @@ internal object DataCommonModule {
fun provideCardCryptoCurrencyFactory(
excludedBlockchains: ExcludedBlockchains,
userWalletsListRepository: UserWalletsListRepository,
accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
userTokensResponseStore: UserTokensResponseStore,
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
): CardCryptoCurrencyFactory {
return DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig,
excludedBlockchains = excludedBlockchains,
userWalletsListRepository = userWalletsListRepository,
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
userTokensResponseStore = userTokensResponseStore,
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
)
}
@ -76,8 +71,6 @@ internal object DataCommonModule {
dispatchers: CoroutineDispatcherProvider,
addressesEnricher: UserTokensResponseAddressesEnricher,
walletServerBinder: WalletServerBinder,
appsFlyerStore: AppsFlyerStore,
accountsFeatureToggles: AccountsFeatureToggles,
): UserTokensSaver {
return UserTokensSaver(
tangemTechApi = tangemTechApi,
@ -85,12 +78,10 @@ internal object DataCommonModule {
userTokensResponseStore = userTokensResponseStore,
dispatchers = dispatchers,
addressesEnricher = addressesEnricher,
accountsFeatureToggles = accountsFeatureToggles,
pushTokensRetryerPool = RetryerPool(
coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default),
),
walletServerBinder = walletServerBinder,
appsFlyerStore = appsFlyerStore,
)
}

View file

@ -11,8 +11,8 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -38,20 +38,16 @@ import org.junit.jupiter.params.ParameterizedTest
internal class DefaultCardCryptoCurrencyFactoryTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk()
private val userTokensResponseStore: UserTokensResponseStore = mockk()
private val excludedBlockchains = ExcludedBlockchains()
private val accountsFeatureToggles = mockk<AccountsFeatureToggles>()
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
private val factory = DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig,
excludedBlockchains = excludedBlockchains,
userWalletsListRepository = userWalletsListRepository,
userTokensResponseStore = userTokensResponseStore,
responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(
networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains),
),
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
)
@ -64,7 +60,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
@BeforeEach
fun init() {
clearMocks(userWalletsListRepository, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri)
clearMocks(userWalletsListRepository, walletAccountsFetcher, iconUri)
mockkStatic(Uri::class)
every { Uri.parse(any()) } returns iconUri
@ -80,12 +76,11 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
// Arrange
val userWallet = createMultiWallet()
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
val userTokensResponse = model.userTokensResponse
val accountsResponse = model.accountsResponse
val network = ethereum.network
every { accountsFeatureToggles.isFeatureEnabled } returns false
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse
// Act
val actual = factory.create(userWalletId = userWallet.walletId, network = network)
@ -97,25 +92,25 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
coVerifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
walletAccountsFetcher.getSaved(userWalletId = userWallet.walletId)
}
}
private fun provideTestModels() = listOf(
CreateTestModel.MultiWallet(userTokensResponse = null, expected = emptyList()),
CreateTestModel.MultiWallet(userTokensResponse = createUserTokensResponse(), expected = emptyList()),
CreateTestModel.MultiWallet(accountsResponse = null, expected = emptyList()),
CreateTestModel.MultiWallet(accountsResponse = createAccountsResponse(), expected = emptyList()),
CreateTestModel.MultiWallet(
userTokensResponse = createUserTokensResponse(currencies = listOf(ethereum)),
accountsResponse = createAccountsResponse(currencies = listOf(ethereum)),
expected = listOf(ethereum),
),
CreateTestModel.MultiWallet(
userTokensResponse = createUserTokensResponse(listOf(element = bitcoin)),
accountsResponse = createAccountsResponse(listOf(element = bitcoin)),
expected = emptyList(),
),
)
private fun createUserTokensResponse(currencies: List<CryptoCurrency> = emptyList()): UserTokensResponse {
return userTokensResponseFactory.createUserTokensResponse(
private fun createAccountsResponse(currencies: List<CryptoCurrency> = emptyList()): GetWalletAccountsResponse {
return createWalletAccountsResponse(
currencies = currencies,
isGroupedByNetwork = false,
isSortedByBalance = false,
@ -150,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
}
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
walletAccountsFetcher.getSaved(userWalletId = any())
}
}
@ -195,7 +190,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
}
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
walletAccountsFetcher.getSaved(userWalletId = any())
}
}
@ -218,7 +213,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
val expected: List<CryptoCurrency>
data class MultiWallet(
val userTokensResponse: UserTokensResponse?,
val accountsResponse: GetWalletAccountsResponse?,
override val expected: List<CryptoCurrency>,
) : CreateTestModel
@ -245,10 +240,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
// Arrange
val userWallet = model.multiWallet
val networks = setOf(ethereum.network, bitcoin.network)
val userTokensResponse = model.userTokensResponse
val accountsResponse = model.accountsResponse
every { accountsFeatureToggles.isFeatureEnabled } returns false
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse
// Act
val actual = runCatching {
@ -273,19 +267,19 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
private fun provideTestModels() = listOf(
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = null,
accountsResponse = null,
expected = Result.success(emptyMap()),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = createUserTokensResponse(),
accountsResponse = createWalletAccountsResponse(emptyList(), false, false),
expected = Result.success(
setOf(ethereum.network, bitcoin.network).associateWith { emptyList() },
),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createMultiWallet(),
userTokensResponse = createUserTokensResponse(currencies = listOf(bitcoin, ethereum)),
accountsResponse = createWalletAccountsResponse(currencies = listOf(bitcoin, ethereum), false, false),
expected = mapOf(
bitcoin.network to listOf(bitcoin),
ethereum.network to listOf(ethereum),
@ -293,12 +287,12 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createSingleWallet(),
userTokensResponse = null,
accountsResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = MockUserWalletFactory.createSingleWalletWithToken(),
userTokensResponse = null,
accountsResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
)
@ -306,7 +300,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
data class CreateCurrenciesForMultiWalletModel(
val multiWallet: UserWallet,
val userTokensResponse: UserTokensResponse?,
val accountsResponse: GetWalletAccountsResponse?,
val expected: Result<Map<Network, List<CryptoCurrency>>>,
)
@ -546,11 +540,44 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
)!!
}
private fun createUserTokensResponse(currencies: List<CryptoCurrency> = emptyList()): UserTokensResponse {
return userTokensResponseFactory.createUserTokensResponse(
currencies = currencies,
isGroupedByNetwork = false,
isSortedByBalance = false,
private fun createWalletAccountsResponse(
currencies: List<CryptoCurrency>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
): GetWalletAccountsResponse {
val tokens = currencies.map { currency ->
userTokensResponseFactory.createResponseToken(currency = currency, accountId = null)
}
return GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
version = 0,
group = if (isGroupedByNetwork) {
UserTokensResponse.GroupType.NETWORK
} else {
UserTokensResponse.GroupType.NONE
},
sort = if (isSortedByBalance) {
UserTokensResponse.SortType.BALANCE
} else {
UserTokensResponse.SortType.MANUAL
},
totalAccounts = 1,
totalArchivedAccounts = 0,
),
accounts = listOf(
WalletAccountDTO(
id = "account_0",
name = "Main",
derivationIndex = 0,
icon = "🏠",
iconColor = "#000000",
tokens = tokens,
totalTokens = tokens.size,
totalNetworks = currencies.map { it.network }.distinct().size,
),
),
unassignedTokens = emptyList(),
)
}

View file

@ -6,9 +6,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -27,11 +25,7 @@ class UserTokensSaverTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true)
private val enricher: UserTokensResponseAddressesEnricher = mockk()
private val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
every { this@mockk.isFeatureEnabled } returns true
}
private val walletServerBinder: WalletServerBinder = mockk()
private val appsFlyerStore: AppsFlyerStore = mockk()
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
tangemTechApi = tangemTechApi,
@ -40,8 +34,6 @@ class UserTokensSaverTest {
dispatchers = TestingCoroutineDispatcherProvider(),
addressesEnricher = enricher,
walletServerBinder = walletServerBinder,
appsFlyerStore = appsFlyerStore,
accountsFeatureToggles = accountsFeatureToggles,
pushTokensRetryerPool = mockk(),
)
@ -121,7 +113,6 @@ class UserTokensSaverTest {
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { accountsFeatureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { enricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>

View file

@ -1,129 +0,0 @@
package com.tangem.data.tokens
import arrow.core.Either
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* Default implementation of [MultiWalletCryptoCurrenciesFetcher]
*
* @property tangemTechApi Tangem Tech API
* @property userTokensResponseStore store of [UserTokensResponse]
* @property userTokensSaver user tokens saver
* @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card
* @property expressServiceFetcher express service loader
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class DefaultMultiWalletCryptoCurrenciesFetcher(
private val demoConfig: DemoConfig,
private val userWalletsListRepository: UserWalletsListRepository,
private val tangemTechApi: TangemTechApi,
private val customTokensMerger: CustomTokensMerger,
private val userTokensResponseStore: UserTokensResponseStore,
private val userTokensSaver: UserTokensSaver,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val expressServiceFetcher: ExpressServiceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesFetcher {
private val userTokensResponseFactory = UserTokensResponseFactory()
override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) {
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) {
createDefaultUserTokensResponse(userWallet = userWallet)
} else {
safeApiCall(
call = {
withContext(dispatchers.io) {
tangemTechApi.getUserTokens(userId = userWallet.walletId.stringValue).bind()
}
},
onError = {
handleFetchTokensError(error = it, userWallet = userWallet)
},
)
}
val compatibleUserTokensResponse = response
.let { it.copy(tokens = it.tokens.distinct()) }
.let { customTokensMerger.mergeIfPresented(userWalletId = userWallet.walletId, response = it) }
userTokensSaver.store(userWalletId = userWallet.walletId, response = compatibleUserTokensResponse)
fetchExpressAssetsByNetworkIds(userWallet = userWallet, userTokens = compatibleUserTokensResponse)
}
private suspend fun UserWallet.Cold.isDemoWalletWithoutSavedTokens(): Boolean {
val isDemoCard = demoConfig.isDemoCardId(cardId = cardId)
return if (isDemoCard) {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = walletId)
response == null
} else {
false
}
}
private suspend fun handleFetchTokensError(error: ApiResponseError, userWallet: UserWallet): UserTokensResponse {
val userWalletId = userWallet.walletId
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
?: createDefaultUserTokensResponse(userWallet = userWallet)
if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId")
userTokensSaver.push(userWalletId, response)
}
return response
}
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
val tokens = userTokens.tokens.mapTo(hashSetOf()) { token ->
ExpressAsset.ID(
networkId = token.networkId,
contractAddress = token.contractAddress,
)
}
expressServiceFetcher.fetch(userWallet = userWallet, assetIds = tokens)
}
private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
return userTokensResponseFactory.createUserTokensResponse(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet),
isGroupedByNetwork = false,
isSortedByBalance = false,
accountId = null,
)
}
}

View file

@ -1,16 +1,8 @@
package com.tangem.data.tokens.di
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher
import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -27,39 +19,16 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
@Singleton
@Provides
fun provideMultiWalletCryptoCurrenciesFetcher(
accountsFeatureToggles: AccountsFeatureToggles,
tangemTechApi: TangemTechApi,
userWalletsListRepository: UserWalletsListRepository,
userTokensResponseStore: UserTokensResponseStore,
userTokensSaver: UserTokensSaver,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
expressServiceFetcher: ExpressServiceFetcher,
walletAccountsFetcher: WalletAccountsFetcher,
expressServiceFetcher: ExpressServiceFetcher,
dispatchers: CoroutineDispatcherProvider,
): MultiWalletCryptoCurrenciesFetcher {
return if (accountsFeatureToggles.isFeatureEnabled) {
AccountListCryptoCurrenciesFetcher(
userWalletsListRepository = userWalletsListRepository,
walletAccountsFetcher = walletAccountsFetcher,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
} else {
DefaultMultiWalletCryptoCurrenciesFetcher(
demoConfig = DemoConfig,
userWalletsListRepository = userWalletsListRepository,
tangemTechApi = tangemTechApi,
customTokensMerger = CustomTokensMerger(
tangemTechApi = tangemTechApi,
userTokensSaver = userTokensSaver,
dispatchers = dispatchers,
),
userTokensResponseStore = userTokensResponseStore,
userTokensSaver = userTokensSaver,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
}
return AccountListCryptoCurrenciesFetcher(
userWalletsListRepository = userWalletsListRepository,
walletAccountsFetcher = walletAccountsFetcher,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
}
}

View file

@ -13,7 +13,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -48,7 +47,6 @@ internal object TokensDataModule {
tokensSaver: UserTokensSaver,
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
@ -63,7 +61,6 @@ internal object TokensDataModule {
userTokensSaver = tokensSaver,
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
accountsFeatureToggles = accountsFeatureToggles,
)
}

View file

@ -3,7 +3,9 @@ package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison
import com.tangem.blockchainsdk.utils.*
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.*
@ -13,7 +15,6 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -51,7 +52,6 @@ internal class DefaultCurrenciesRepository(
private val userTokensSaver: UserTokensSaver,
private val userTokensResponseStore: UserTokensResponseStore,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
@ -187,19 +187,7 @@ internal class DefaultCurrenciesRepository(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin {
return if (accountsFeatureToggles.isFeatureEnabled) {
getNetworkCoinNew(userWalletId, networkId, derivationPath)
} else {
getNetworkCoinLegacy(userWalletId, networkId, derivationPath)
}
}
private suspend fun getNetworkCoinNew(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin = withContext(dispatchers.default) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
)
.orEmpty()
@ -211,44 +199,6 @@ internal class DefaultCurrenciesRepository(
?: error("Unable to find coin for network ID: $networkId")
}
private suspend fun getNetworkCoinLegacy(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin {
return withContext(dispatchers.io) {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
val blockchain = networkId.toBlockchain()
val blockchainNetworkId = blockchain.toNetworkId()
val coinId = blockchain.toCoinId()
val storedCoin = storedTokens.tokens
.find { token ->
token.networkId == blockchainNetworkId &&
compareIdWithMigrations(token, coinId) &&
token.derivationPath == derivationPath.value
} ?: error("Coin in this network $networkId not found")
val coin = responseCryptoCurrenciesFactory.createCurrency(
responseToken = storedCoin,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
}
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
@ -470,13 +420,6 @@ internal class DefaultCurrenciesRepository(
)
}
private fun compareIdWithMigrations(token: UserTokensResponse.Token, coinId: String): Boolean {
return when {
token.id == OLD_POLYGON_NAME -> NEW_POLYGON_NAME == coinId
else -> token.id == coinId
}
}
private suspend fun fetchTokens(userWallet: UserWallet) {
val userWalletId = userWallet.walletId

View file

@ -1,549 +0,0 @@
package com.tangem.data.tokens
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.test.core.assertEither
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val userTokensResponseFactory = UserTokensResponseFactory()
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val tangemTechApi: TangemTechApi = mockk()
private val customTokensMerger: CustomTokensMerger = mockk()
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val expressServiceFetcher: ExpressServiceFetcher = mockk(relaxUnitFun = true)
private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher(
demoConfig = DemoConfig,
userWalletsListRepository = userWalletsListRepository,
tangemTechApi = tangemTechApi,
customTokensMerger = customTokensMerger,
userTokensResponseStore = userTokensResponseStore,
userTokensSaver = userTokensSaver,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
expressServiceFetcher = expressServiceFetcher,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@BeforeEach
fun resetMocks() {
clearMocks(
userWalletsListRepository,
tangemTechApi,
userTokensResponseStore,
userTokensSaver,
cardCryptoCurrencyFactory,
expressServiceFetcher,
)
}
@Test
fun `fetch failure if UserWallet ISN'T MULTI-CURRENCY wallet`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns false
}
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
// Act
val actual = fetcher(params)
// Assert
val expected = IllegalStateException(
"${DefaultMultiWalletCryptoCurrenciesFetcher::class.simpleName} supports only multi-currency wallet",
).left()
assertEither(actual, expected)
verifyOrder { userWalletsListRepository.userWallets }
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(any())
}
}
@Test
fun `fetch successfully if CARD IS DEMO and STORED TOKENS ARE EMPTY`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "AC01000000041225"
}
val defaultCoins = listOf(
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
cryptoCurrencyFactory.createCoin(Blockchain.Ethereum),
)
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = listOf(
userTokensResponseFactory.createResponseToken(defaultCoins.first()),
userTokensResponseFactory.createResponseToken(defaultCoins.last()),
),
)
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null
every {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
} returns defaultCoins
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
} returns userTokensResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = userTokensResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
}
}
@Test
fun `fetch successfully if CARD IS DEMO and STORED TOKENS AREN'T EMPTY`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "AC01000000041225"
}
val apiResponse = ApiResponse.Success(
data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN),
)
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
} returns apiResponse.data
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = defaultResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId)
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
@Test
fun `fetch successfully if CARD ISN'T DEMO`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "cardID"
}
val apiResponse = ApiResponse.Success(
data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN),
)
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
} returns apiResponse.data
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = apiResponse.data.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
userTokensResponseStore.getSyncOrNull(userWalletId = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
@Test
fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "cardID"
}
@Suppress("UNCHECKED_CAST")
val apiResponse = ApiResponse.Error(
cause = ApiResponseError.TimeoutException(),
) as ApiResponse<UserTokensResponse>
val defaultCoins = listOf(
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
cryptoCurrencyFactory.createCoin(Blockchain.Ethereum),
)
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = listOf(
userTokensResponseFactory.createResponseToken(defaultCoins.first()),
userTokensResponseFactory.createResponseToken(defaultCoins.last()),
),
)
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null
coEvery {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
} returns defaultCoins
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
} returns userTokensResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = userTokensResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
}
coVerify(inverse = true) {
userTokensSaver.push(userWalletId = any(), response = any())
}
}
@Test
fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "cardID"
}
@Suppress("UNCHECKED_CAST")
val apiResponse = ApiResponse.Error(
cause = ApiResponseError.TimeoutException(),
) as ApiResponse<UserTokensResponse>
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
} returns defaultResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = defaultResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
userTokensSaver.push(userWalletId = any(), response = any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
@Test
fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "cardID"
}
@Suppress("UNCHECKED_CAST")
val apiResponse = ApiResponse.Error(
cause = ApiResponseError.HttpException(
code = ApiResponseError.HttpException.Code.NOT_FOUND,
message = null,
errorBody = null,
),
) as ApiResponse<UserTokensResponse>
val defaultCoins = listOf(
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
cryptoCurrencyFactory.createCoin(Blockchain.Ethereum),
)
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = listOf(
userTokensResponseFactory.createResponseToken(defaultCoins.first()),
userTokensResponseFactory.createResponseToken(defaultCoins.last()),
),
)
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null
coEvery {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
} returns defaultCoins
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
} returns userTokensResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = userTokensResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
}
}
@Test
fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet.Cold> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
every { cardId } returns "cardID"
}
@Suppress("UNCHECKED_CAST")
val apiResponse = ApiResponse.Error(
cause = ApiResponseError.HttpException(
code = ApiResponseError.HttpException.Code.NOT_FOUND,
message = null,
errorBody = null,
),
) as ApiResponse<UserTokensResponse>
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse
coEvery {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
} returns defaultResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = defaultResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
assertEither(actual, expected)
coVerifyOrder {
userWalletsListRepository.userWallets
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
}
}
private companion object {
val userWalletId = UserWalletId("011")
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = listOf(
UserTokensResponse.Token(
id = null,
networkId = "bitcoin",
derivationPath = null,
name = "Bitcoin",
symbol = "BTC",
decimals = 8,
contractAddress = null,
addresses = listOf(),
),
),
)
fun UserTokensResponse.toAssetId(): Set<ExpressAsset.ID> {
return tokens.mapTo(hashSetOf()) { token ->
ExpressAsset.ID(
networkId = token.networkId,
contractAddress = token.contractAddress,
)
}
}
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.domain.account.featuretoggle
/**
* Accounts feature toggle
*
[REDACTED_AUTHOR]
*/
interface AccountsFeatureToggles {
val isFeatureEnabled: Boolean
}

View file

@ -2,7 +2,6 @@ package com.tangem.domain.account.usecase
import arrow.core.Option
import arrow.core.getOrElse
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.loadAndGet
@ -18,20 +17,16 @@ import kotlinx.coroutines.flow.*
*
* @property crudRepository repository to perform CRUD operations on accounts.
* @property userWalletsListRepository repository to get the list of user wallets.
* @property accountsFeatureToggles feature toggles for accounts.
*
[REDACTED_AUTHOR]
*/
class IsAccountsModeEnabledUseCase(
private val crudRepository: AccountsCRUDRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<Boolean> {
if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(value = false)
return userWalletsListRepository.loadAndGet()
.flatMapLatest { userWallets ->
val totalAccountsCountList = getTotalAccountsCountList(userWallets)
@ -43,8 +38,6 @@ class IsAccountsModeEnabledUseCase(
}
suspend fun invokeSync(): Boolean {
if (!accountsFeatureToggles.isFeatureEnabled) return false
return userWalletsListRepository.userWallets.value.orEmpty()
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0

View file

@ -3,7 +3,6 @@ package com.tangem.domain.account.usecase
import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
@ -12,7 +11,6 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
@ -26,47 +24,26 @@ class IsAccountsModeEnabledUseCaseTest {
private val accountsCRUDRepository: AccountsCRUDRepository = mockk()
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val featureToggles: AccountsFeatureToggles = mockk()
private val useCase = IsAccountsModeEnabledUseCase(
crudRepository = accountsCRUDRepository,
userWalletsListRepository = userWalletsListRepository,
accountsFeatureToggles = featureToggles,
)
@AfterEach
fun tearDown() {
clearMocks(userWalletsListRepository, accountsCRUDRepository, featureToggles)
clearMocks(userWalletsListRepository, accountsCRUDRepository)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Invoke {
@Test
fun `returns false when feature is disabled`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns false
// Act
val actual = useCase.invoke().firstOrNull()
// Assert
Truth.assertThat(actual).isFalse()
verify(exactly = 1) { featureToggles.isFeatureEnabled }
coVerify(inverse = true) {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
}
@Test
fun `returns false when loadAndGet emits one wallet with isMultiCurrency false`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
// Act
@ -76,7 +53,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
@ -89,7 +65,6 @@ class IsAccountsModeEnabledUseCaseTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some())
@ -100,7 +75,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isTrue()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId)
@ -112,7 +86,6 @@ class IsAccountsModeEnabledUseCaseTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none())
@ -123,7 +96,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId)
@ -136,7 +108,6 @@ class IsAccountsModeEnabledUseCaseTest {
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some())
@ -147,7 +118,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isTrue()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId)
@ -161,25 +131,9 @@ class IsAccountsModeEnabledUseCaseTest {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class InvokeSync {
@Test
fun `returns false when feature is disabled`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns false
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verify(exactly = 1) { featureToggles.isFeatureEnabled }
verify(inverse = true) { userWalletsListRepository.userWallets.value }
}
@Test
fun `returns false when getUserWalletsSync returns empty list`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets.value } returns emptyList()
// Act
@ -189,7 +143,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.userWallets.value
}
@ -201,7 +154,6 @@ class IsAccountsModeEnabledUseCaseTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
// Act
@ -211,7 +163,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.userWallets.value
}
@ -223,7 +174,6 @@ class IsAccountsModeEnabledUseCaseTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns 2.some()
@ -234,7 +184,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isTrue()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId)
}
@ -245,7 +194,6 @@ class IsAccountsModeEnabledUseCaseTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns none()
@ -256,7 +204,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isFalse()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId)
}
@ -268,7 +215,6 @@ class IsAccountsModeEnabledUseCaseTest {
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { userWalletsListRepository.userWallets.value } returns listOf(wallet1, wallet2)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } returns 2.some()
@ -279,7 +225,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isTrue()
coVerifyOrder {
featureToggles.isFeatureEnabled
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId)
}

View file

@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable
/**
* Temporary wrapper over ID to support a gradual migration between two modes:
*
* - [Wallet] legacy flow, wallet design; used when the [AccountsFeatureToggles] is disabled.
* - [Account] new flow, wallet/account design; used when the [AccountsFeatureToggles] is enabled.
* - [Wallet] legacy flow, wallet design.
* - [Account] new flow, wallet/account design.
*
* When an [Account] you must verify the current app mode with [IsAccountsModeEnabledUseCase]
* and then use wallet/account design
* When using an [Account], you must verify the current app mode with [IsAccountsModeEnabledUseCase],
* then use the wallet/account design.
*
* Intended to be removed after the full migration to the new mode.
*/