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

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