Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-06 10:59:48 +04:00
parent e2721b21e1
commit a67ca6279f
14 changed files with 24 additions and 614 deletions

View file

@ -23,7 +23,6 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -138,8 +137,6 @@ interface ApplicationEntryPoint {
fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore
fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk

View file

@ -39,7 +39,6 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
@ -218,9 +217,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val apiConfigsManager: ApiConfigsManager
get() = entryPoint.getApiConfigsManager()
private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore()
private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository()
@ -381,7 +377,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
settingsManager = settingsManager,
uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore,
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy,

View file

@ -8,7 +8,6 @@ import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -63,9 +62,7 @@ internal object NFTDomainModule {
fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository,
singleAccountListSupplier: SingleAccountListSupplier,
currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountListSupplier = singleAccountListSupplier,
)

View file

@ -13,7 +13,6 @@ import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
@ -71,7 +70,6 @@ data class DaggerGraphState(
val uiMessageSender: UiMessageSender? = null,
val cardArworksProvider: CardArtworksProvider? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
val userTokensResponseStore: UserTokensResponseStore? = null,
val userWalletsListRepository: UserWalletsListRepository? = null,
val tangemHotSdk: TangemHotSdk? = null,
val trackingContextProxy: TrackingContextProxy? = null,

View file

@ -1,71 +0,0 @@
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
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.flow.FlowProducerTools
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.*
/**
* Default implementation of [MultiWalletCryptoCurrenciesProducer]
*
* @property params params
* @property flowProducerTools tools for producing flows
* @property userWalletsListRepository repository for getting user wallets
* @property userTokensResponseStore store of `UserTokensResponse`
* @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse`
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constructor(
@Assisted val params: MultiWalletCryptoCurrenciesProducer.Params,
override val flowProducerTools: FlowProducerTools,
private val userWalletsListRepository: UserWalletsListRepository,
private val userTokensResponseStore: UserTokensResponseStore,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesProducer {
override val fallback: Option<Set<CryptoCurrency>> = emptySet<CryptoCurrency>().some()
override fun produce(): Flow<Set<CryptoCurrency>> {
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
if (!userWallet.isMultiCurrency) {
error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet")
}
return userTokensResponseStore.get(userWalletId = params.userWalletId)
.distinctUntilChanged()
.map { response ->
if (response == null) return@map emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
response = response,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
).toSet()
}
.onEmpty { emit(emptySet()) }
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : MultiWalletCryptoCurrenciesProducer.Factory {
override fun create(
params: MultiWalletCryptoCurrenciesProducer.Params,
): DefaultMultiWalletCryptoCurrenciesProducer
}
}

View file

@ -1,378 +0,0 @@
package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.test.core.getEmittedValues
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWallet.walletId)
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk()
private val flowProducerTools: FlowProducerTools = mockk()
private val producer = DefaultMultiWalletCryptoCurrenciesProducer(
params = params,
flowProducerTools = flowProducerTools,
userWalletsListRepository = userWalletsListRepository,
userTokensResponseStore = userTokensResponseStore,
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@BeforeEach
fun resetMocks() {
clearMocks(userWalletsListRepository, userTokensResponseStore, responseCryptoCurrenciesFactory)
}
@Test
fun `flow is mapped for user wallet id from params`() = runTest {
// Arrange
val userTokensResponseFlow = flowOf<UserTokensResponse?>(null)
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = emptySet<CryptoCurrency>()
Truth.assertThat(actual.size).isEqualTo(1)
Truth.assertThat(actual.first()).isEqualTo(expected)
verifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.get(params.userWalletId)
}
verify(inverse = true) {
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any(), accountIndex = any())
}
}
@Test
fun `flow will updated if UserTokensResponse is updated`() = runTest {
// Arrange
val userTokensResponseFlow = MutableSharedFlow<UserTokensResponse>(replay = 2)
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.TOKEN,
sort = UserTokensResponse.SortType.MARKETCAP,
tokens = emptyList(),
)
val cryptoCurrencies = emptySet<CryptoCurrency>()
val updatedUserTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.TOKEN,
sort = UserTokensResponse.SortType.MARKETCAP,
tokens = listOf(
UserTokensResponse.Token(
id = null,
networkId = "bitcoin",
derivationPath = null,
name = "Bitcoin",
symbol = "BTC",
decimals = 8,
contractAddress = null,
addresses = listOf(),
),
),
)
val updatedCryptoCurrencies = setOf(
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
)
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
} returns cryptoCurrencies.toList()
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = updatedUserTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
} returns updatedCryptoCurrencies.toList()
val producerFlow = producer.produce()
// Act 1 (first emit)
userTokensResponseFlow.emit(userTokensResponse)
val actual1 = getEmittedValues(flow = producerFlow)
// Assert
val expected1 = cryptoCurrencies
Truth.assertThat(actual1.size).isEqualTo(1)
Truth.assertThat(actual1.first()).isEqualTo(expected1)
verifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.get(params.userWalletId)
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
}
// Act 2 (second emit)
userTokensResponseFlow.emit(updatedUserTokensResponse)
val actual2 = getEmittedValues(flow = producerFlow)
// Assert
val expected2 = listOf(cryptoCurrencies, updatedCryptoCurrencies)
Truth.assertThat(actual2.size).isEqualTo(2)
Truth.assertThat(actual2).isEqualTo(expected2)
verifyOrder {
responseCryptoCurrenciesFactory.createCurrencies(
response = updatedUserTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
}
}
@Test
fun `flow is filtered the same status`() = runTest {
// Arrange
val userTokensResponseFlow = MutableSharedFlow<UserTokensResponse>(replay = 2)
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.TOKEN,
sort = UserTokensResponse.SortType.MARKETCAP,
tokens = emptyList(),
)
val cryptoCurrencies = emptySet<CryptoCurrency>()
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
} returns cryptoCurrencies.toList()
val producerFlow = producer.produce()
// Act 1 (first emit)
userTokensResponseFlow.emit(userTokensResponse)
val actual1 = getEmittedValues(flow = producerFlow)
// Assert
val expected1 = cryptoCurrencies
Truth.assertThat(actual1.size).isEqualTo(1)
Truth.assertThat(actual1.first()).isEqualTo(expected1)
verifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.get(params.userWalletId)
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
}
// Act 2 (second emit)
userTokensResponseFlow.emit(userTokensResponse)
val actual2 = getEmittedValues(flow = producerFlow)
// Assert
val expected2 = expected1
Truth.assertThat(actual2.size).isEqualTo(1)
Truth.assertThat(actual2.first()).isEqualTo(expected2)
}
@Disabled
@Test
fun `flow throws exception`() = runTest {
// Arrange
val exception = IllegalStateException()
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.TOKEN,
sort = UserTokensResponse.SortType.MARKETCAP,
tokens = emptyList(),
)
val cryptoCurrencies = emptySet<CryptoCurrency>()
val innerFlow = MutableStateFlow(value = false)
val userTokensResponseFlow = flow {
if (innerFlow.value) {
emit(userTokensResponse)
} else {
throw exception
}
}
.buffer(capacity = 5)
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
every {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
} returns cryptoCurrencies.toList()
val producerFlow = producer.produceWithFallback()
// Act 1 (fallback)
val actual1 = getEmittedValues(flow = producerFlow)
// Assert
val expected1 = producer.fallback.getOrNull()
Truth.assertThat(actual1.size).isEqualTo(1)
Truth.assertThat(actual1.first()).isEqualTo(expected1)
verifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.get(params.userWalletId)
}
// Act 2 (emit)
innerFlow.emit(value = true)
val actual2 = getEmittedValues(flow = producerFlow)
// Assert
val expected2 = cryptoCurrencies
Truth.assertThat(actual2.size).isEqualTo(1)
Truth.assertThat(actual2.first()).isEqualTo(expected2)
verifyOrder {
responseCryptoCurrenciesFactory.createCurrencies(
response = userTokensResponse,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
}
}
@Test
fun `flow is empty if store returns empty flow`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { userTokensResponseStore.get(params.userWalletId) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = producer.fallback.getOrNull()
Truth.assertThat(actual.size).isEqualTo(1)
Truth.assertThat(actual.first()).isEqualTo(expected)
verifyOrder {
userWalletsListRepository.userWallets
userTokensResponseStore.get(params.userWalletId)
}
verify(inverse = true) {
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any(), accountIndex = any())
}
}
@Test
fun `produce throws exception if UserWallet isn't multi-currency wallet`() = runTest {
// Arrange
val mockUserWallet = mockk<UserWallet> {
every { walletId } returns userWallet.walletId
every { isMultiCurrency } returns false
}
val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
// Act
val actual = runCatching { producer.produce() }.exceptionOrNull()
// Assert
val expected = IllegalStateException(
"${DefaultMultiWalletCryptoCurrenciesProducer::class.simpleName} supports only multi-currency wallet",
)
Truth.assertThat(actual).isInstanceOf(expected::class.java)
Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message)
verifyOrder { userWalletsListRepository.userWallets }
verify(inverse = true) {
userTokensResponseStore.get(any())
responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any(), accountIndex = any())
}
}
private companion object {
val scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(2),
derivedKeys = emptyMap(),
)
val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse)
}
}

View file

@ -1,14 +1,12 @@
package com.tangem.data.common.currency
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.response.ApiResponse
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.local.token.UserTokensResponseStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncOrNull
import com.tangem.domain.models.wallet.UserWallet
@ -23,31 +21,11 @@ import timber.log.Timber
class UserTokensSaver(
private val tangemTechApi: TangemTechApi,
private val userWalletsListRepository: UserWalletsListRepository,
private val userTokensResponseStore: UserTokensResponseStore,
private val dispatchers: CoroutineDispatcherProvider,
private val addressesEnricher: UserTokensResponseAddressesEnricher,
private val walletServerBinder: WalletServerBinder,
private val pushTokensRetryerPool: RetryerPool,
) {
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
suspend fun storeAndPush(userWalletId: UserWalletId, response: UserTokensResponse) {
withContext(dispatchers.default) {
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = true)
store(userWalletId = userWalletId, response = enrichedResponse, useEnricher = false)
push(userWalletId = userWalletId, response = enrichedResponse, useEnricher = false)
}
}
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true) =
withContext(dispatchers.default) {
val updatedResponse = response
.applyCompatibility()
.enrichIf(userWalletId = userWalletId, condition = useEnricher)
userTokensResponseStore.store(userWalletId = userWalletId, response = updatedResponse)
}
suspend fun push(
userWalletId: UserWalletId,
@ -111,10 +89,6 @@ class UserTokensSaver(
)
}
private fun UserTokensResponse.applyCompatibility(): UserTokensResponse {
return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this)
}
private suspend fun UserTokensResponse.enrichIf(
userWalletId: UserWalletId,
condition: Boolean,

View file

@ -12,7 +12,6 @@ import com.tangem.data.common.wallet.WalletServerBinder
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.common.wallets.UserWalletsListRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -67,7 +66,6 @@ internal object DataCommonModule {
fun provideUserTokensSaver(
tangemTechApi: TangemTechApi,
userWalletsListRepository: UserWalletsListRepository,
userTokensResponseStore: UserTokensResponseStore,
dispatchers: CoroutineDispatcherProvider,
addressesEnricher: UserTokensResponseAddressesEnricher,
walletServerBinder: WalletServerBinder,
@ -75,7 +73,6 @@ internal object DataCommonModule {
return UserTokensSaver(
tangemTechApi = tangemTechApi,
userWalletsListRepository = userWalletsListRepository,
userTokensResponseStore = userTokensResponseStore,
dispatchers = dispatchers,
addressesEnricher = addressesEnricher,
pushTokensRetryerPool = RetryerPool(

View file

@ -6,7 +6,6 @@ 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.token.UserTokensResponseStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -23,14 +22,12 @@ class UserTokensSaverTest {
private val tangemTechApi: TangemTechApi = mockk()
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true)
private val enricher: UserTokensResponseAddressesEnricher = mockk()
private val walletServerBinder: WalletServerBinder = mockk()
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
tangemTechApi = tangemTechApi,
userWalletsListRepository = userWalletsListRepository,
userTokensResponseStore = userTokensResponseStore,
dispatchers = TestingCoroutineDispatcherProvider(),
addressesEnricher = enricher,
walletServerBinder = walletServerBinder,
@ -42,46 +39,11 @@ class UserTokensSaverTest {
clearMocks(
tangemTechApi,
userWalletsListRepository,
userTokensResponseStore,
enricher,
walletServerBinder,
)
}
@Test
fun `GIVEN user wallet id and response WHEN store THEN should store enriched response`() = runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.MANUAL,
tokens = emptyList(),
)
coEvery { enricher(userWalletId, response) } returns enrichedResponse
// WHEN
userTokensSaver.store(userWalletId, response)
// THEN
coVerifyOrder {
enricher(userWalletId, response)
userTokensResponseStore.store(userWalletId, enrichedResponse)
}
coVerify(inverse = true) {
tangemTechApi.saveTokens(any(), any())
}
}
@Test
fun `GIVEN user wallet id and response WHEN push AND api call fails THEN should log error and call onFailSend`() =
runTest {
@ -133,49 +95,4 @@ class UserTokensSaverTest {
assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" }
}
@Test
fun `GIVEN user wallet id and response WHEN storeAndPush THEN should store and push enriched response`() = runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val userWallet = mockk<UserWallet.Cold> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.name } returns "Wallet"
}
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
walletName = userWallet.name,
walletType = WalletType.COLD,
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
walletName = userWallet.name,
walletType = WalletType.COLD,
)
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { enricher(userWalletId, response) } returns enrichedResponse
coEvery {
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
} returns ApiResponse.Success(Unit)
// WHEN
userTokensSaver.storeAndPush(userWalletId, response)
// THEN
coVerifyOrder {
enricher(userWalletId, response)
userWalletsListRepository.userWallets
tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse)
}
}
}

View file

@ -1,40 +1,30 @@
package com.tangem.domain.nft
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTNetworks
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
class GetNFTNetworksUseCase(
private val currenciesRepository: CurrenciesRepository,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val nftRepository: NFTRepository,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(portfolioId: PortfolioId): Flow<NFTNetworks> = when (portfolioId) {
is PortfolioId.Account -> {
singleAccountListSupplier(portfolioId.userWalletId)
operator fun invoke(accountId: AccountId): Flow<NFTNetworks> {
return singleAccountListSupplier(accountId.userWalletId)
.mapNotNull { accountList ->
val account = accountList.accounts.find { it.accountId == portfolioId.accountId }
val account = accountList.accounts.find { it.accountId == accountId }
(account as? Account.CryptoPortfolio)?.cryptoCurrencies?.toList()
}
.mapLatest { it.toNFTNetworks(portfolioId.userWalletId) }
}
is PortfolioId.Wallet -> {
currenciesRepository
.getWalletCurrenciesUpdates(portfolioId.userWalletId)
.map { cryptoCurrencies -> cryptoCurrencies.toNFTNetworks(portfolioId.userWalletId) }
}
.mapLatest { it.toNFTNetworks(accountId.userWalletId) }
}
private suspend fun List<CryptoCurrency>.toNFTNetworks(userWalletId: UserWalletId): NFTNetworks {

View file

@ -20,7 +20,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.PortfolioId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.PortfolioSelectorController
@ -160,13 +159,13 @@ internal class DefaultNFTComponent @AssistedInject constructor(
if (portfolioData.isSingleChoice) {
val mainAccountId = portfolioData.balances.values.first()
.accountsBalance.mainAccount.account.accountId
innerRouter.push(NFTRoute.Receive(portfolioId = PortfolioId(mainAccountId)))
innerRouter.push(NFTRoute.Receive(accountId = mainAccountId))
} else {
bottomSheetNavigation.activate(Unit)
val selectedAccountId = portfolioSelectorController.selectedAccount
.filterNotNull().first()
bottomSheetNavigation.dismiss()
innerRouter.push(NFTRoute.Receive(portfolioId = PortfolioId(selectedAccountId)))
innerRouter.push(NFTRoute.Receive(accountId = selectedAccountId))
}
}.saveIn(onReceiveClickJob)
@ -176,7 +175,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
): ComposableContentComponent = NFTReceiveComponent(
context = factoryContext,
params = NFTReceiveComponent.Params(
portfolioId = route.portfolioId,
accountId = route.accountId,
onBackClick = ::onChildBack,
),
tokenReceiveComponentFactory = tokenReceiveComponentFactory,

View file

@ -1,10 +1,10 @@
package com.tangem.features.nft.common
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
internal sealed class NFTRoute : Route {
@ -15,9 +15,7 @@ internal sealed class NFTRoute : Route {
) : NFTRoute()
@Serializable
data class Receive(
val portfolioId: PortfolioId,
) : NFTRoute()
data class Receive(val accountId: AccountId) : NFTRoute()
@Serializable
data class Details(

View file

@ -13,8 +13,8 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.receive.ui.NFTReceive
import com.tangem.features.tokenreceive.TokenReceiveComponent
@ -57,7 +57,7 @@ internal class NFTReceiveComponent @AssistedInject constructor(
)
data class Params(
val portfolioId: PortfolioId,
val accountId: AccountId,
val onBackClick: () -> Unit,
)
}

View file

@ -18,7 +18,6 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.network.Network
@ -88,14 +87,12 @@ internal class NFTReceiveModel @Inject constructor(
}
private fun loadPortfolioName() = modelScope.launch(dispatchers.default) {
val appBarSubtitle = when (val portfolioId = params.portfolioId) {
is PortfolioId.Wallet -> loadWalletName(portfolioId.userWalletId)
is PortfolioId.Account -> if (isAccountsModeEnabledUseCase.invokeSync()) {
loadAccountName(portfolioId.accountId)
val appBarSubtitle = if (isAccountsModeEnabledUseCase.invokeSync()) {
loadAccountName(params.accountId)
} else {
loadWalletName(portfolioId.userWalletId)
}
loadWalletName(params.accountId.userWalletId)
}
_state.update { it.copy(appBarSubtitle = appBarSubtitle) }
}
@ -121,7 +118,7 @@ internal class NFTReceiveModel @Inject constructor(
private fun subscribeToNFTAvailableNetworks() {
combine(
flow = getNFTNetworksUseCase(params.portfolioId),
flow = getNFTNetworksUseCase(params.accountId),
flow2 = searchManager.query.distinctUntilChanged(),
) { networks, query ->
filterNFTAvailableNetworksUseCase(networks, query)
@ -173,7 +170,7 @@ internal class NFTReceiveModel @Inject constructor(
analyticsEventHandler.send(NFTAnalyticsEvent.Receive.BlockchainChosen(network.name))
val networkStatus = getNFTNetworkStatusUseCase.invoke(
userWalletId = params.portfolioId.userWalletId,
userWalletId = params.accountId.userWalletId,
network = network,
) ?: return@launch
@ -198,7 +195,7 @@ internal class NFTReceiveModel @Inject constructor(
private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig {
val cryptoCurrency = getNFTCurrencyUseCase.invoke(network)
return receiveAddressesFactory.createForNft(
userWalletId = params.portfolioId.userWalletId,
userWalletId = params.accountId.userWalletId,
addresses = addresses,
network = network,
nft = cryptoCurrency,