Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-16 13:32:13 +04:00
parent a5a8cee12c
commit 9b1bf5813f
15 changed files with 895 additions and 30 deletions

View file

@ -1,10 +1,13 @@
package com.tangem.common.test.domain.wallet
import com.tangem.common.card.WalletData
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
/**
@ -29,4 +32,29 @@ object MockUserWalletFactory {
hasBackupError = false,
)
}
fun createSingleWalletWithToken(): UserWallet.Cold {
return UserWallet.Cold(
name = "NODL",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
).copy(
productType = ProductType.Note,
walletData = WalletData(
blockchain = "ETH",
token = WalletData.Token(
name = "Ethereum",
symbol = "ETH",
contractAddress = "0x",
decimals = 8,
),
),
),
hasBackupError = false,
)
}
}

View file

@ -23,6 +23,7 @@ dependencies {
// region Project - Domain
api(projects.domain.account)
api(projects.domain.card)
api(projects.domain.models)
// endregion

View file

@ -0,0 +1,28 @@
package com.tangem.data.account.di
import com.tangem.data.account.producer.DefaultMultiAccountListProducer
import com.tangem.data.account.producer.DefaultSingleAccountListProducer
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.account.producer.SingleAccountListProducer
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AccountListProducerFactoryModule {
@Binds
@Singleton
fun bindSingleAccountListProducerFactory(
impl: DefaultSingleAccountListProducer.Factory,
): SingleAccountListProducer.Factory
@Binds
@Singleton
fun bindMultiAccountListProducerFactory(
impl: DefaultMultiAccountListProducer.Factory,
): MultiAccountListProducer.Factory
}

View file

@ -0,0 +1,34 @@
package com.tangem.data.account.di
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AccountListSupplierModule {
@Provides
@Singleton
fun provideSingleAccountListSupplier(factory: SingleAccountListProducer.Factory): SingleAccountListSupplier {
return object : SingleAccountListSupplier(
factory = factory,
keyCreator = { "single_account_list_${it.userWalletId.stringValue}" },
) {}
}
@Provides
@Singleton
fun provideMultiNetworkStatusSupplier(factory: MultiAccountListProducer.Factory): MultiAccountListSupplier {
return object : MultiAccountListSupplier(
factory = factory,
keyCreator = { "multi_networks_statuses" },
) {}
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
/**
* Default implementation of [MultiAccountListProducer].
* Produces a list of [AccountList]s for all user wallets.
*
* @property params params
* @property userWalletsStore store that provides user wallets
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultMultiAccountListProducer @AssistedInject constructor(
@Assisted val params: Unit,
private val userWalletsStore: UserWalletsStore,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiAccountListProducer {
override val fallback: Option<List<AccountList>> = emptyList<AccountList>().some()
@OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<List<AccountList>> {
return userWalletsStore.userWallets
.distinctUntilChanged()
.flatMapLatest { userWallets ->
combine(
flows = userWallets.map(walletAccountListFlowFactory::create),
transform = ::listOf,
)
}
.distinctUntilChanged()
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : MultiAccountListProducer.Factory {
override fun create(params: Unit): DefaultMultiAccountListProducer
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.none
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
/**
* Default implementation of [SingleAccountListProducer].
* Produces a list of [AccountList] for a specific user wallet.
*
* @property params params containing the user wallet ID
* @property userWalletsStore store that provides user wallets
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultSingleAccountListProducer @AssistedInject constructor(
@Assisted val params: SingleAccountListProducer.Params,
private val userWalletsStore: UserWalletsStore,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleAccountListProducer {
override val fallback: Option<AccountList> = none()
@OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<AccountList> {
return userWalletsStore.userWallets
.mapNotNull { userWallets ->
userWallets.firstOrNull { it.walletId == params.userWalletId }
}
.flatMapLatest(walletAccountListFlowFactory::create)
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : SingleAccountListProducer.Factory {
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.data.account.producer
import com.tangem.data.account.converter.AccountListConverter
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.models.wallet.requireColdWallet
import kotlinx.coroutines.flow.*
import javax.inject.Inject
/**
* Factory that creates a flow of [AccountList] for a specific [UserWallet]
*
* @property accountsResponseStoreFactory factory to create [AccountsResponseStore]
* @property accountListConverterFactory factory to create [AccountListConverter]
* @property cardCryptoCurrencyFactory factory to create supported crypto currencies for a card
*
[REDACTED_AUTHOR]
*/
internal class WalletAccountListFlowFactory @Inject constructor(
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
private val accountListConverterFactory: AccountListConverter.Factory,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
) {
fun create(userWallet: UserWallet): Flow<AccountList> {
return if (userWallet.isMultiCurrency) {
createForMultiWallet(userWallet)
} else {
flowOf(createForSingleWallet(userWallet))
}
}
private fun createForMultiWallet(userWallet: UserWallet): Flow<AccountList> {
val converter by lazy { accountListConverterFactory.create(userWallet) }
return accountsResponseStoreFactory.create(userWallet.walletId).data
.filterNotNull()
.distinctUntilChanged()
.map(converter::convert)
}
private fun createForSingleWallet(userWallet: UserWallet): AccountList {
val isSingleWalletWithToken = userWallet.requireColdWallet().cardTypesResolver.isSingleWalletWithToken()
val currencies = if (isSingleWalletWithToken) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet).toSet()
} else {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet).let(::setOf)
}
return AccountList.empty(userWallet = userWallet, cryptoCurrencies = currencies)
}
}

View file

@ -0,0 +1,222 @@
package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultMultiAccountListProducerTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
private val producer = DefaultMultiAccountListProducer(
params = Unit,
userWalletsStore = userWalletsStore,
walletAccountListFlowFactory = walletAccountListFlowFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val userWalletId = UserWalletId("011")
private val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
}
@AfterEach
fun tearDownEach() {
clearMocks(userWalletsStore, walletAccountListFlowFactory)
}
@Test
fun produce() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = listOf(accountList)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow will updated if factoryFlow is updated`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
val firstEmission = producer.produce().let(::getEmittedValues)
// Assert (first emission)
Truth.assertThat(firstEmission).containsExactly(listOf(accountList))
// Act (second emission)
factoryFlow.value = updatedAccountList
val secondEmission = producer.produce().let(::getEmittedValues)
// Assert (second emission)
Truth.assertThat(secondEmission).containsExactly(listOf(updatedAccountList))
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is filtered the same response`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
val firstEmission = producer.produce().let(::getEmittedValues)
// Assert (first emission)
Truth.assertThat(firstEmission).containsExactly(listOf(accountList))
// Act (second emission) - the same status
factoryFlow.value = accountList
val secondEmission = producer.produce().let(::getEmittedValues)
// Assert (second emission)
Truth.assertThat(secondEmission).containsExactly(listOf(accountList))
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow returns empty list if factory throws exception`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWallet) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
// Assert
val expected = emptyList<AccountList>()
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is empty if userWalletsFlow returns empty flow`() = runTest {
// Arrange
val userWalletsFlow = emptyFlow<List<UserWallet>>()
every { userWalletsStore.userWallets } returns userWalletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(exactly = 1) { userWalletsStore.userWallets }
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
}
@Test
fun `flow is empty if factory returns empty flow`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
every { walletAccountListFlowFactory.create(userWallet) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is empty if one of factoryFlow is empty`() = runTest {
// Arrange
val userWalletId2 = UserWalletId("012")
val userWallet2 = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId2
}
val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
every { walletAccountListFlowFactory.create(userWallet2) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWallet2)
}
}
}

View file

@ -0,0 +1,198 @@
package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultSingleAccountListProducerTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
private val userWalletId = UserWalletId("011")
private val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
}
private val producer = DefaultSingleAccountListProducer(
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
userWalletsStore = userWalletsStore,
walletAccountListFlowFactory = walletAccountListFlowFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@AfterEach
fun tearDownEach() {
clearMocks(userWalletsStore, walletAccountListFlowFactory)
}
@Test
fun produce() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = accountList
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow will updated if factoryFlow is updated`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
val firstEmission = producer.produce().let(::getEmittedValues)
// Assert (first emission)
Truth.assertThat(firstEmission).containsExactly(accountList)
// Act (second emission)
factoryFlow.value = updatedAccountList
val secondEmission = producer.produce().let(::getEmittedValues)
// Assert (second emission)
Truth.assertThat(secondEmission).containsExactly(updatedAccountList)
coVerifyOrder {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is filtered the same response`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
val firstEmission = producer.produce().let(::getEmittedValues)
// Assert (first emission)
Truth.assertThat(firstEmission).containsExactly(accountList)
// Act (second emission) - the same status
factoryFlow.value = accountList
val secondEmission = producer.produce().let(::getEmittedValues)
// Assert (second emission)
Truth.assertThat(secondEmission).containsExactly(accountList)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is empty if factory throws exception`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWallet) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is empty if userWalletsFlow returns empty flow`() = runTest {
// Arrange
val userWalletsFlow = emptyFlow<List<UserWallet>>()
every { userWalletsStore.userWallets } returns userWalletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(exactly = 1) { userWalletsStore.userWallets }
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
}
@Test
fun `flow is empty if userWalletsFlow doesn't contains userWalletId from params`() = runTest {
// Arrange
val unknownId = UserWalletId("012")
val unknownWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns unknownId
}
val userWalletsFlow = MutableStateFlow(listOf(unknownWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
}
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
}
}

View file

@ -0,0 +1,148 @@
package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.data.account.converter.AccountListConverter
import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WalletAccountListFlowFactoryTest {
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
private val accountsResponseStore: AccountsResponseStore = mockk()
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
private val accountListConverterFactory: AccountListConverter.Factory = mockk()
private val accountListConverter: AccountListConverter = mockk()
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val factory = WalletAccountListFlowFactory(
accountsResponseStoreFactory = accountsResponseStoreFactory,
accountListConverterFactory = accountListConverterFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
)
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
@AfterEach
fun tearDownEach() {
clearMocks(accountListConverter)
accountsResponseStoreFlow.value = null
}
@Test
fun `create for multi wallet`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.isMultiCurrency } returns true
}
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
every { accountsResponseStore.data } returns accountsResponseStoreFlow
accountsResponseStoreFlow.value = accountsResponse
val accountList = AccountList.empty(userWallet)
every { accountListConverterFactory.create(userWallet) } returns accountListConverter
every { accountListConverter.convert(accountsResponse) } returns accountList
// Act
val actual = factory.create(userWallet).let(::getEmittedValues)
// Assert
val expected = accountList
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
accountListConverterFactory.create(userWallet)
accountListConverter.convert(accountsResponse)
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(any())
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(any())
}
}
@Test
fun `create for single wallet`() = runTest {
val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false)
val currency = cryptoCurrencyFactory.ethereum
every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency
// Act
val actual = factory.create(userWallet).let(::getEmittedValues)
// Assert
val expected = AccountList.empty(userWallet = userWallet, cryptoCurrencies = setOf(currency))
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet)
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = any())
accountsResponseStoreFactory.create(any())
accountsResponseStore.data
accountListConverterFactory.create(any())
accountListConverter.convert(any())
}
}
@Test
fun `flow is created for single wallet with token`() = runTest {
val nodl = MockUserWalletFactory.createSingleWalletWithToken()
val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet()
every {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl)
} returns currencies.toList()
// Act
val actual = factory.create(nodl).let(::getEmittedValues)
// Assert
val expected = AccountList.empty(userWallet = nodl, cryptoCurrencies = currencies)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl)
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(any())
accountsResponseStoreFactory.create(any())
accountsResponseStore.data
accountListConverterFactory.create(any())
accountListConverter.convert(any())
}
}
}

View file

@ -7,13 +7,14 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.card.WalletData
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.common.test.utils.ProvideTestModels
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.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -159,7 +160,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
model: CreateTestModel.SingleWalletWithToken,
) = runTest {
// Arrange
val userWallet = createSingleWalletWithToken()
val userWallet = MockUserWalletFactory.createSingleWalletWithToken()
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
@ -284,7 +285,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
CreateCurrenciesForMultiWalletModel(
multiWallet = createSingleWalletWithToken(),
multiWallet = MockUserWalletFactory.createSingleWalletWithToken(),
userTokensResponse = null,
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
),
@ -456,7 +457,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
expected = Result.failure(IllegalArgumentException("Coin for the single currency card cannot be null")),
),
CreateForSingleWalletWithTokenModel(
singleWalletWithToken = createSingleWalletWithToken(),
singleWalletWithToken = MockUserWalletFactory.createSingleWalletWithToken(),
isPrimaryTokenExpected = true,
expected = Result.success(listOf(ethereum)),
),
@ -522,33 +523,8 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
)
}
private fun createSingleWalletWithToken(): UserWallet.Cold {
return UserWallet.Cold(
name = "NODL",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
).copy(
productType = ProductType.Note,
walletData = WalletData(
blockchain = "ETH",
token = WalletData.Token(
name = "Ethereum",
symbol = "ETH",
contractAddress = "0x",
decimals = 8,
),
),
),
hasBackupError = false,
)
}
private fun createPrimaryToken(blockchain: Blockchain): CryptoCurrency.Token {
val userWallet = createSingleWalletWithToken()
val userWallet = MockUserWalletFactory.createSingleWalletWithToken()
return CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = userWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,

View file

@ -0,0 +1,14 @@
package com.tangem.domain.account.producer
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.core.flow.FlowProducer
/**
* Produces a list of [AccountList]s for all user wallets.
*
[REDACTED_AUTHOR]
*/
interface MultiAccountListProducer : FlowProducer<List<AccountList>> {
interface Factory : FlowProducer.Factory<Unit, MultiAccountListProducer>
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.account.producer
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.wallet.UserWalletId
/**
* Produces a list of [AccountList] for a specific user wallet.
*
[REDACTED_AUTHOR]
*/
interface SingleAccountListProducer : FlowProducer<AccountList> {
data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, SingleAccountListProducer>
}

View file

@ -0,0 +1,21 @@
package com.tangem.domain.account.supplier
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.core.flow.FlowCachingSupplier
import kotlinx.coroutines.flow.Flow
/**
* Supplier that provides a list of [AccountList]s for all user wallets.
*
[REDACTED_AUTHOR]
*/
abstract class MultiAccountListSupplier(
override val factory: MultiAccountListProducer.Factory,
override val keyCreator: (Unit) -> String,
) : FlowCachingSupplier<MultiAccountListProducer, Unit, List<AccountList>>() {
operator fun invoke(): Flow<List<AccountList>> {
return super.invoke(params = Unit)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.account.supplier
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.core.flow.FlowCachingSupplier
/**
* Supplier that provides a single [AccountList] for a specific user wallet.
*
[REDACTED_AUTHOR]
*/
abstract class SingleAccountListSupplier(
override val factory: SingleAccountListProducer.Factory,
override val keyCreator: (SingleAccountListProducer.Params) -> String,
) : FlowCachingSupplier<SingleAccountListProducer, SingleAccountListProducer.Params, AccountList>()