diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 17ca52c9ce..0bb1450c46 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -9,12 +9,17 @@ android { namespace = "com.tangem.data.txhistory" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { implementation(projects.data.common) implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.core.pagination) + implementation(projects.core.analytics) implementation(projects.domain.legacy) implementation(projects.domain.common) @@ -24,6 +29,9 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.account) + implementation(projects.domain.account.status) implementation(projects.libs.blockchainSdk) @@ -34,4 +42,11 @@ dependencies { implementation(deps.hilt.core) kapt(deps.hilt.kapt) + + // region Test + testImplementation(projects.common.test) + testImplementation(projects.test.core) + testImplementation(projects.test.mock) + testRuntimeOnly(deps.test.junit5.engine) + // endregion } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index 7eab581f07..31d1f0e300 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -1,51 +1,35 @@ package com.tangem.data.txhistory.di -import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.txhistory.fetcher.DefaultAppTxHistoryFetcher +import com.tangem.data.txhistory.fetcher.DefaultTxHistoryFetcherUtils +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository import com.tangem.data.txhistory.repository.RefactoredTxHistoryRepository -import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.txhistory.fetcher.AppTxHistoryFetcher import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Binds 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 TxHistoryDataModule { +internal interface TxHistoryDataModule { - @Provides + @Binds @Singleton - fun provideTxHistoryRepository( - cacheRegistry: CacheRegistry, - walletManagersFacade: WalletManagersFacade, - userWalletsListRepository: UserWalletsListRepository, - txHistoryItemsStore: TxHistoryItemsStore, - dispatchers: CoroutineDispatcherProvider, - ): TxHistoryRepository = DefaultTxHistoryRepository( - cacheRegistry = cacheRegistry, - walletManagersFacade = walletManagersFacade, - userWalletsListRepository = userWalletsListRepository, - txHistoryItemsStore = txHistoryItemsStore, - dispatchers = dispatchers, - ) + fun provideTxHistoryRepository(default: DefaultTxHistoryRepository): TxHistoryRepository - @Provides + @Binds @Singleton - fun provideTxHistoryRepositoryV2( - walletManagersFacade: WalletManagersFacade, - dispatchers: CoroutineDispatcherProvider, - txHistoryItemsStore: TxHistoryItemsStore, - cacheRegistry: CacheRegistry, - ): TxHistoryRepositoryV2 = RefactoredTxHistoryRepository( - walletManagersFacade = walletManagersFacade, - dispatchers = dispatchers, - txHistoryItemsStore = txHistoryItemsStore, - cacheRegistry = cacheRegistry, - ) + fun provideTxHistoryRepositoryV2(default: RefactoredTxHistoryRepository): TxHistoryRepositoryV2 + + @Binds + @Singleton + fun provideAppTxHistoryFetcher(default: DefaultAppTxHistoryFetcher): AppTxHistoryFetcher + + @Binds + fun provideTxHistoryFetcherUtils(default: DefaultTxHistoryFetcherUtils): TxHistoryFetcherUtils } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultAccountTxHistoryFetcher.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultAccountTxHistoryFetcher.kt new file mode 100644 index 0000000000..89cd675194 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultAccountTxHistoryFetcher.kt @@ -0,0 +1,125 @@ +package com.tangem.data.txhistory.fetcher + +import androidx.annotation.VisibleForTesting +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTrigger +import com.tangem.domain.account.supplier.SingleAccountSupplier +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.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase +import com.tangem.domain.txhistory.fetcher.AccountTxHistoryFetcher +import com.tangem.domain.txhistory.fetcher.ExpressTxHistoryFetcher +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.domain.walletmanager.WalletManagersFacade +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.* +import java.util.concurrent.ConcurrentHashMap + +internal class DefaultAccountTxHistoryFetcher @AssistedInject constructor( + @Assisted override val accountId: AccountId, + private val utils: TxHistoryFetcherUtils, + private val singleAccountSupplier: SingleAccountSupplier, + private val paymentAccountCurrency: GetPaymentAccountCryptoCurrencyStatusUseCase, + private val expressFetcherFactory: DefaultExpressTxHistoryFetcher.Factory, + private val walletManagersFacade: WalletManagersFacade, +) : AccountTxHistoryFetcher, TxHistoryFetcherUtils by utils { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val expressFetchers = ConcurrentHashMap() + + init { + defaultLaunchIn(buildFlow()) + } + + override suspend fun invoke(params: TxHistoryFetchTrigger) { + sendTrigger(params) + } + + override fun close() { + cancelScope() + expressFetchers.forEach { (_, fetcher) -> fetcher.close() } + expressFetchers.clear() + } + + private fun buildFlow(): Flow = channelFlow { + val accountFlow = singleAccountSupplier(accountId).stateIn(this) + + val controlFetchersFlow = when (accountFlow.value) { + is Account.CryptoPortfolio -> accountFlow + .filterIsInstance() + .controlFetchersForCryptoAccount() + is Account.Payment -> controlFetchersForPaymentAccount() + } + controlFetchersFlow.launchIn(this) + + receiveTrigger().onEach { trigger -> + when (trigger) { + is TxHistoryFetchTrigger.TokenDetailsOpen -> { + val addressKey = getAddress(trigger.walletId, trigger.currency) ?: return@onEach + expressFetchers[addressKey]?.invoke(trigger) + } + is TxHistoryFetchTrigger.TokenDetailsPTR -> { + val addressKey = getAddress(trigger.walletId, trigger.currency) ?: return@onEach + expressFetchers[addressKey]?.invoke(trigger) + } + } + }.collect {} + } + + private fun controlFetchersForPaymentAccount(): Flow { + return paymentAccountCurrency(walletId) + .map { (_, paymentCurrency) -> + val paymentNetwork = paymentCurrency.currency.network + val address = getAddress(walletId, paymentCurrency.currency) + if (paymentNetwork.isSupportExpressTxHistory() && !address.isNullOrBlank()) { + getOrPutExpressFetcher(address, accountId) + } else { + // single currency for payment account, so we can close all(one) + expressFetchers.forEach { (_, fetcher) -> fetcher.close() } + expressFetchers.clear() + } + } + } + + private fun Flow.controlFetchersForCryptoAccount(): Flow { + return map { account -> account.cryptoCurrencies } + .map { currencies -> + val onlyCoins = currencies.filterIsInstance() + val networks = onlyCoins.map { coin -> coin.network } + val newExpressKeys = networks + .filter { net -> net.isSupportExpressTxHistory() } + .mapNotNull { net -> getAddress(walletId, net) } + .toSet() + val previousExpressKeys = expressFetchers.keys + val removed = previousExpressKeys - newExpressKeys + newExpressKeys.forEach { address -> getOrPutExpressFetcher(address, accountId) } + removed.forEach { address -> expressFetchers.remove(address)?.close() } + } + } + + @Suppress("FunctionOnlyReturningConstant") // todo txhistory check + private fun Network.isSupportExpressTxHistory(): Boolean { + return true + } + + private suspend fun getAddress(userWalletId: UserWalletId, currencies: CryptoCurrency): String? = + getAddress(userWalletId, currencies.network) + + private suspend fun getAddress(userWalletId: UserWalletId, network: Network): String? = + walletManagersFacade.getDefaultAddress(userWalletId, network) + + private fun getOrPutExpressFetcher(address: String, id: AccountId): ExpressTxHistoryFetcher { + return expressFetchers.computeIfAbsent(address) { expressFetcherFactory.create(address, id) } + } + + @AssistedFactory + internal interface Factory { + fun create(accountId: AccountId): DefaultAccountTxHistoryFetcher + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultAppTxHistoryFetcher.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultAppTxHistoryFetcher.kt new file mode 100644 index 0000000000..c6f9992f7a --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultAppTxHistoryFetcher.kt @@ -0,0 +1,88 @@ +package com.tangem.data.txhistory.fetcher + +import androidx.annotation.VisibleForTesting +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTrigger +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.txhistory.fetcher.AppTxHistoryFetcher +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.domain.txhistory.fetcher.WalletTxHistoryFetcher +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import kotlinx.coroutines.flow.* +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +internal class DefaultAppTxHistoryFetcher @Inject constructor( + private val utils: TxHistoryFetcherUtils, + private val getWalletsUseCase: GetWalletsUseCase, + private val selectedWalletUseCase: GetSelectedWalletUseCase, + private val walletTxHistoryFetcherFactory: DefaultWalletTxHistoryFetcher.Factory, +) : AppTxHistoryFetcher, TxHistoryFetcherUtils by utils { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val fetchers = ConcurrentHashMap() + + init { + defaultLaunchIn(buildFlow()) + } + + override suspend fun invoke(params: TxHistoryFetchTrigger) { + sendTrigger(params) + } + + override fun close() { + cancelScope() + fetchers.forEach { (_, fetcher) -> fetcher.close() } + fetchers.clear() + } + + private fun buildFlow(): Flow = channelFlow { + val walletsFlow: StateFlow> = getWalletsUseCase + .invokeAsMap(isOnlyMultiCurrency = true, filterLocked = true) + .stateIn(this) + + selectedWalletUseCase.selectedFlow() + .filter { wallet -> wallet.isMultiCurrency } + // todo txhistory some init trigger? + .onEach { } + .launchIn(this) + + walletsFlow + .map { map -> map.keys } + .distinctUntilChanged() + // todo txhistory create for all or lazy? + .createForNewWallets() + .closeForRemovedWallets() + .launchIn(this) + + receiveTrigger() + .onEach { trigger -> + when (trigger) { + is TxHistoryFetchTrigger.TokenDetailsOpen -> fetchers[trigger.walletId]?.invoke(trigger) + is TxHistoryFetchTrigger.TokenDetailsPTR -> fetchers[trigger.walletId]?.invoke(trigger) + } + } + .collect {} + } + + private fun Flow>.createForNewWallets() = + onEach { ids -> ids.forEach { walletId -> getOrPutFetcher(walletId) } } + + private fun Flow>.closeForRemovedWallets() = runningReduce { previousIds, newIds -> + val removedWallets = previousIds.subtract(newIds) + removedWallets.forEach { walletId -> fetchers.remove(walletId)?.close() } + newIds + } + + private fun getOrPutFetcher(id: UserWalletId): WalletTxHistoryFetcher { + return fetchers.computeIfAbsent(id) { createFetcher(id) } + } + + private fun createFetcher(id: UserWalletId): WalletTxHistoryFetcher { + return walletTxHistoryFetcherFactory.create(id) + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultExpressTxHistoryFetcher.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultExpressTxHistoryFetcher.kt new file mode 100644 index 0000000000..146b881d68 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultExpressTxHistoryFetcher.kt @@ -0,0 +1,30 @@ +package com.tangem.data.txhistory.fetcher + +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.txhistory.fetcher.ExpressTxHistoryFetcher +import com.tangem.domain.txhistory.fetcher.TxHistoryExpressTrigger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultExpressTxHistoryFetcher @AssistedInject constructor( + @Assisted override val address: String, + @Assisted private val accountId: AccountId, + private val utils: TxHistoryFetcherUtils, +) : ExpressTxHistoryFetcher, TxHistoryFetcherUtils by utils { + + override suspend fun invoke(params: TxHistoryExpressTrigger) { + utils.sendTrigger(params) + accountId + } + + override fun close() { + cancelScope() + } + + @AssistedFactory + internal interface Factory { + fun create(address: String, accountId: AccountId): DefaultExpressTxHistoryFetcher + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultWalletTxHistoryFetcher.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultWalletTxHistoryFetcher.kt new file mode 100644 index 0000000000..4f83626cf6 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/DefaultWalletTxHistoryFetcher.kt @@ -0,0 +1,98 @@ +package com.tangem.data.txhistory.fetcher + +import androidx.annotation.VisibleForTesting +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn +import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTrigger +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.status.utils.AccountCryptoCurrencyOperations.getAccountCryptoCurrency +import com.tangem.domain.account.supplier.SingleAccountListSupplier +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.txhistory.fetcher.AccountTxHistoryFetcher +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.domain.txhistory.fetcher.WalletTxHistoryFetcher +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.* +import java.util.concurrent.ConcurrentHashMap + +internal class DefaultWalletTxHistoryFetcher @AssistedInject constructor( + @Assisted override val walletId: UserWalletId, + private val utils: TxHistoryFetcherUtils, + private val singleAccountListSupplier: SingleAccountListSupplier, + private val accountTxHistoryFetcher: DefaultAccountTxHistoryFetcher.Factory, +) : WalletTxHistoryFetcher, TxHistoryFetcherUtils by utils { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val fetchers = ConcurrentHashMap() + + init { + defaultLaunchIn(buildFlow()) + } + + override suspend fun invoke(params: TxHistoryFetchTrigger) { + sendTrigger(params) + } + + override fun close() { + cancelScope() + fetchers.forEach { (_, fetcher) -> fetcher.close() } + fetchers.clear() + } + + private fun buildFlow(): Flow = channelFlow { + val accountListFlow = singleAccountListSupplier(walletId) + .stateIn(this) + + fun accountList(): AccountList = accountListFlow.value + + accountListFlow + .map { accountList -> accountList.accounts.mapTo(mutableSetOf()) { account -> account.accountId } } + .distinctUntilChanged() + .createForNewAccounts() + .closeForRemovedAccounts() + .launchIn(this) + + receiveTrigger() + .onEach { trigger -> + when (trigger) { + is TxHistoryFetchTrigger.TokenDetailsOpen -> accountList() + .findFetcher(trigger.currency)?.invoke(trigger) + is TxHistoryFetchTrigger.TokenDetailsPTR -> accountList() + .findFetcher(trigger.currency)?.invoke(trigger) + } + } + .collect {} + } + + private fun Flow>.createForNewAccounts() = + onEach { ids -> ids.forEach { id -> getOrPutFetcher(id) } } + + private fun Flow>.closeForRemovedAccounts() = runningReduce { previousIds, newIds -> + val removedWallets = previousIds.subtract(newIds) + removedWallets.forEach { walletId -> fetchers.remove(walletId)?.close() } + newIds + } + + private fun AccountList.findFetcher(currency: CryptoCurrency): AccountTxHistoryFetcher? = this + .getAccountCryptoCurrency(currency) + .getOrNull() + ?.account + ?.let { account -> fetchers[account.accountId] } + + private fun getOrPutFetcher(id: AccountId): AccountTxHistoryFetcher { + return fetchers.computeIfAbsent(id) { createFetcher(id) } + } + + private fun createFetcher(id: AccountId): AccountTxHistoryFetcher { + return accountTxHistoryFetcher.create(id) + } + + @AssistedFactory + internal interface Factory { + fun create(walletId: UserWalletId): DefaultWalletTxHistoryFetcher + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/TxHistoryFetcherUtils.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/TxHistoryFetcherUtils.kt new file mode 100644 index 0000000000..51c163f9f5 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/fetcher/TxHistoryFetcherUtils.kt @@ -0,0 +1,67 @@ +package com.tangem.data.txhistory.fetcher + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.plus +import javax.inject.Inject + +const val TX_HISTORY_TAG = "TxHistory" + +internal interface TxHistoryFetcherUtils { + + val triggersBuffer: Channel + + val fetcherScope: CoroutineScope + val analyticsEventHandler: AnalyticsEventHandler + val analyticsExceptionHandler: AnalyticsExceptionHandler + + suspend fun sendTrigger(trigger: TxHistoryFetchTrigger) + + companion object { + + fun TxHistoryFetcherUtils.cancelScope() = fetcherScope.cancel() + + fun TxHistoryFetcherUtils.defaultLaunchIn(flow: Flow) = flow + .retry { error -> + logError(error) + true + } + .launchIn(fetcherScope) + + fun TxHistoryFetcherUtils.receiveTrigger(): Flow { + return triggersBuffer.receiveAsFlow() + } + + inline fun TxHistoryFetcherUtils.receiveTriggerInstance(): Flow { + return receiveTrigger().filterIsInstance() + } + + fun logError(error: Throwable, message: String = error.message.orEmpty()) { + TangemLogger.withTag(TX_HISTORY_TAG).e(message, error) + } + } +} + +internal class DefaultTxHistoryFetcherUtils @Inject constructor( + appScope: AppCoroutineScope, + override val analyticsEventHandler: AnalyticsEventHandler, + override val analyticsExceptionHandler: AnalyticsExceptionHandler, +) : TxHistoryFetcherUtils { + + override val triggersBuffer: Channel = Channel(Channel.BUFFERED) + + // todo txhistory use lifecycle scope? + override val fetcherScope: CoroutineScope = appScope + SupervisorJob() + + override suspend fun sendTrigger(trigger: TxHistoryFetchTrigger) { + triggersBuffer.trySend(trigger) + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 1763c2dd4b..6280beb4f6 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -23,8 +23,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import javax.inject.Inject -class DefaultTxHistoryRepository( +class DefaultTxHistoryRepository @Inject constructor( private val cacheRegistry: CacheRegistry, private val walletManagersFacade: WalletManagersFacade, private val userWalletsListRepository: UserWalletsListRepository, diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt index 88b9b82391..6fcc14bd8e 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt @@ -17,8 +17,9 @@ import com.tangem.pagination.BatchListSource import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger +import javax.inject.Inject -internal class RefactoredTxHistoryRepository( +internal class RefactoredTxHistoryRepository @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val txHistoryItemsStore: TxHistoryItemsStore, private val cacheRegistry: CacheRegistry, diff --git a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultAccountTxHistoryFetcherTest.kt b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultAccountTxHistoryFetcherTest.kt new file mode 100644 index 0000000000..5b8a4953db --- /dev/null +++ b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultAccountTxHistoryFetcherTest.kt @@ -0,0 +1,176 @@ +package com.tangem.data.txhistory.fetcher + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.TestAppCoroutineScope +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.domain.account.supplier.SingleAccountSupplier +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.test.mock.MockAccounts +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.job +import kotlinx.coroutines.test.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultAccountTxHistoryFetcherTest { + + private val singleAccountSupplier: SingleAccountSupplier = mockk() + private val paymentAccountCurrency: GetPaymentAccountCryptoCurrencyStatusUseCase = mockk() + private val expressFetcherFactory: DefaultExpressTxHistoryFetcher.Factory = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + + private val coin: CryptoCurrency = MockCryptoCurrencyFactory().ethereum + + private val cryptoAccount = MockAccounts.createAccount( + derivationIndex = 1, + userWalletId = WALLET_ID, + cryptoCurrencies = listOf(coin), + ) + + @BeforeEach + fun setup() { + clearMocks(singleAccountSupplier, paymentAccountCurrency, expressFetcherFactory, walletManagersFacade) + } + + @Test + fun `creates express fetcher per coin address of a crypto portfolio account`() = runTest { + val utils = createUtils() + val accountFlow = MutableStateFlow(cryptoAccount) + every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow + coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS + val expressFetcher = relaxedExpressFetcher() + every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher + + // Act + val fetcher = createFetcher(cryptoAccount.accountId, utils) + advanceUntilIdle() + + // Assert + assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS) + verify(exactly = 1) { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } + } + + @Test + fun `closes express fetcher when its coin is removed from the account`() = runTest { + val utils = createUtils() + val accountFlow = MutableStateFlow(cryptoAccount) + every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow + coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS + val expressFetcher = relaxedExpressFetcher() + every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher + + val fetcher = createFetcher(cryptoAccount.accountId, utils) + advanceUntilIdle() + assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS) + + // Act + accountFlow.value = cryptoAccount.copy(cryptoCurrencies = emptyList()) + advanceUntilIdle() + + // Assert + assertThat(fetcher.expressFetchers).isEmpty() + verify(exactly = 1) { expressFetcher.close() } + } + + @Test + fun `routes trigger to the express fetcher of the currency address`() = runTest { + val utils = createUtils() + val accountFlow = MutableStateFlow(cryptoAccount) + every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow + coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS + val expressFetcher = relaxedExpressFetcher() + every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher + + val fetcher = createFetcher(cryptoAccount.accountId, utils) + advanceUntilIdle() + + // Act + val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID, currency = coin) + fetcher.invoke(trigger) + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { expressFetcher.invoke(trigger) } + } + + @Test + fun `creates express fetcher for a payment account currency`() = runTest { + val utils = createUtils() + val paymentAccountId = AccountId.forPaymentAccount(WALLET_ID) + val accountFlow = MutableStateFlow(Account.Payment(WALLET_ID)) + every { singleAccountSupplier.invoke(paymentAccountId) } returns accountFlow + + val paymentStatus = mockk(relaxed = true) + val currencyStatus = mockk { every { currency } returns coin } + every { paymentAccountCurrency.invoke(WALLET_ID) } returns flowOf(paymentStatus to currencyStatus) + coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS + val expressFetcher = relaxedExpressFetcher() + every { expressFetcherFactory.create(ADDRESS, paymentAccountId) } returns expressFetcher + + // Act + val fetcher = createFetcher(paymentAccountId, utils) + advanceUntilIdle() + + // Assert + assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS) + verify(exactly = 1) { expressFetcherFactory.create(ADDRESS, paymentAccountId) } + } + + @Test + fun `close cancels scope and closes all express fetchers`() = runTest { + val utils = createUtils() + val accountFlow = MutableStateFlow(cryptoAccount) + every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow + coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS + val expressFetcher = relaxedExpressFetcher() + every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher + + val fetcher = createFetcher(cryptoAccount.accountId, utils) + advanceUntilIdle() + assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS) + + // Act + fetcher.close() + + // Assert + assertThat(fetcher.expressFetchers).isEmpty() + verify(exactly = 1) { expressFetcher.close() } + assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse() + } + + private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils( + appScope = TestAppCoroutineScope(testScope = this), + analyticsEventHandler = mockk(relaxed = true), + analyticsExceptionHandler = mockk(relaxed = true), + ) + + private fun createFetcher(accountId: AccountId, utils: DefaultTxHistoryFetcherUtils) = + DefaultAccountTxHistoryFetcher( + accountId = accountId, + utils = utils, + singleAccountSupplier = singleAccountSupplier, + paymentAccountCurrency = paymentAccountCurrency, + expressFetcherFactory = expressFetcherFactory, + walletManagersFacade = walletManagersFacade, + ) + + private fun relaxedExpressFetcher() = mockk(relaxed = true) + + private companion object { + val WALLET_ID = MockAccounts.userWalletId + const val ADDRESS = "0xEthAddress" + } +} \ No newline at end of file diff --git a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultAppTxHistoryFetcherTest.kt b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultAppTxHistoryFetcherTest.kt new file mode 100644 index 0000000000..8176ea0589 --- /dev/null +++ b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultAppTxHistoryFetcherTest.kt @@ -0,0 +1,164 @@ +package com.tangem.data.txhistory.fetcher + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.TestAppCoroutineScope +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.job +import kotlinx.coroutines.test.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultAppTxHistoryFetcherTest { + + private val getWalletsUseCase: GetWalletsUseCase = mockk() + private val selectedWalletUseCase: GetSelectedWalletUseCase = mockk() + private val walletFetcherFactory: DefaultWalletTxHistoryFetcher.Factory = mockk() + + private val currency: CryptoCurrency = MockCryptoCurrencyFactory().ethereum + + @BeforeEach + fun setup() { + clearMocks(getWalletsUseCase, selectedWalletUseCase, walletFetcherFactory) + every { selectedWalletUseCase.selectedFlow() } returns emptyFlow() + } + + @Test + fun `creates wallet fetcher for each new wallet`() = runTest { + val utils = createUtils() + val walletsFlow = MutableStateFlow(linkedMapOf()) + every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow + val walletFetcher1 = relaxedWalletFetcher() + every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1 + + val fetcher = createFetcher(utils) + advanceUntilIdle() + assertThat(fetcher.fetchers).isEmpty() + + // Act + walletsFlow.value = linkedMapOf(WALLET_ID_1 to mockk()) + advanceUntilIdle() + + // Assert + assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1) + verify(exactly = 1) { walletFetcherFactory.create(WALLET_ID_1) } + } + + @Test + fun `closes and removes fetcher when wallet is removed`() = runTest { + val utils = createUtils() + val walletsFlow = MutableStateFlow( + linkedMapOf(WALLET_ID_1 to mockk(), WALLET_ID_2 to mockk()), + ) + every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow + val walletFetcher1 = relaxedWalletFetcher() + val walletFetcher2 = relaxedWalletFetcher() + every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1 + every { walletFetcherFactory.create(WALLET_ID_2) } returns walletFetcher2 + + val fetcher = createFetcher(utils) + advanceUntilIdle() + assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1, WALLET_ID_2) + + // Act + walletsFlow.value = linkedMapOf(WALLET_ID_1 to mockk()) + advanceUntilIdle() + + // Assert + assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1) + verify(exactly = 1) { walletFetcher2.close() } + verify(inverse = true) { walletFetcher1.close() } + } + + @Test + fun `routes trigger to the fetcher of the target wallet`() = runTest { + val utils = createUtils() + val walletsFlow = MutableStateFlow(linkedMapOf(WALLET_ID_1 to mockk())) + every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow + val walletFetcher1 = relaxedWalletFetcher() + every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1 + + val fetcher = createFetcher(utils) + advanceUntilIdle() + + // Act + val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID_1, currency = currency) + fetcher.invoke(trigger) + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { walletFetcher1.invoke(trigger) } + } + + @Test + fun `does nothing when trigger targets unknown wallet`() = runTest { + val utils = createUtils() + val walletsFlow = MutableStateFlow(linkedMapOf()) + every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow + + val fetcher = createFetcher(utils) + advanceUntilIdle() + + // Act + val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID_1, currency = currency) + val result = fetcher.invoke(trigger) + advanceUntilIdle() + + // Assert + assertThat(fetcher.fetchers).isEmpty() + verify(inverse = true) { walletFetcherFactory.create(any()) } + } + + @Test + fun `close cancels scope and closes all child fetchers`() = runTest { + val utils = createUtils() + val walletsFlow = MutableStateFlow(linkedMapOf(WALLET_ID_1 to mockk())) + every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow + val walletFetcher1 = relaxedWalletFetcher() + every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1 + + val fetcher = createFetcher(utils) + advanceUntilIdle() + assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1) + + // Act + fetcher.close() + + // Assert + assertThat(fetcher.fetchers).isEmpty() + verify(exactly = 1) { walletFetcher1.close() } + assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse() + } + + private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils( + appScope = TestAppCoroutineScope(testScope = this), + analyticsEventHandler = mockk(relaxed = true), + analyticsExceptionHandler = mockk(relaxed = true), + ) + + private fun createFetcher(utils: DefaultTxHistoryFetcherUtils) = DefaultAppTxHistoryFetcher( + utils = utils, + getWalletsUseCase = getWalletsUseCase, + selectedWalletUseCase = selectedWalletUseCase, + walletTxHistoryFetcherFactory = walletFetcherFactory, + ) + + private fun relaxedWalletFetcher() = mockk(relaxed = true) + + private companion object { + val WALLET_ID_1 = UserWalletId("001") + val WALLET_ID_2 = UserWalletId("002") + } +} \ No newline at end of file diff --git a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultWalletTxHistoryFetcherTest.kt b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultWalletTxHistoryFetcherTest.kt new file mode 100644 index 0000000000..279714b077 --- /dev/null +++ b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/fetcher/DefaultWalletTxHistoryFetcherTest.kt @@ -0,0 +1,176 @@ +package com.tangem.data.txhistory.fetcher + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.TestAppCoroutineScope +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger +import com.tangem.test.mock.MockAccounts +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.job +import kotlinx.coroutines.test.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultWalletTxHistoryFetcherTest { + + private val singleAccountListSupplier: SingleAccountListSupplier = mockk() + private val accountFetcherFactory: DefaultAccountTxHistoryFetcher.Factory = mockk() + + private val currency: CryptoCurrency = MockCryptoCurrencyFactory().ethereum + + private val mainAccount = Account.CryptoPortfolio.createMainAccount( + userWalletId = WALLET_ID, + cryptoCurrencies = listOf(currency), + ) + private val secondAccount = MockAccounts.createAccount(derivationIndex = 1, userWalletId = WALLET_ID) + + @BeforeEach + fun setup() { + clearMocks(singleAccountListSupplier, accountFetcherFactory) + } + + @Test + fun `creates account fetcher for each account in the wallet`() = runTest { + val utils = createUtils() + val accountListFlow = MutableStateFlow(accountListOf(mainAccount, secondAccount)) + every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow + val mainFetcher = relaxedAccountFetcher() + val secondFetcher = relaxedAccountFetcher() + every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher + every { accountFetcherFactory.create(secondAccount.accountId) } returns secondFetcher + + // Act + val fetcher = createFetcher(utils) + advanceUntilIdle() + + // Assert + assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId, secondAccount.accountId) + verify(exactly = 1) { accountFetcherFactory.create(mainAccount.accountId) } + verify(exactly = 1) { accountFetcherFactory.create(secondAccount.accountId) } + } + + @Test + fun `closes and removes fetcher when account is removed`() = runTest { + val utils = createUtils() + val accountListFlow = MutableStateFlow(accountListOf(mainAccount, secondAccount)) + every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow + val mainFetcher = relaxedAccountFetcher() + val secondFetcher = relaxedAccountFetcher() + every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher + every { accountFetcherFactory.create(secondAccount.accountId) } returns secondFetcher + + val fetcher = createFetcher(utils) + advanceUntilIdle() + assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId, secondAccount.accountId) + + // Act + accountListFlow.value = accountListOf(mainAccount) + advanceUntilIdle() + + // Assert + assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId) + verify(exactly = 1) { secondFetcher.close() } + verify(inverse = true) { mainFetcher.close() } + } + + @Test + fun `routes trigger to the fetcher of the account that holds the currency`() = runTest { + val utils = createUtils() + val accountListFlow = MutableStateFlow(accountListOf(mainAccount)) + every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow + val mainFetcher = relaxedAccountFetcher() + every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher + + val fetcher = createFetcher(utils) + advanceUntilIdle() + + // Act + val trigger = TxHistoryFetchTrigger.TokenDetailsPTR(walletId = WALLET_ID, currency = currency) + fetcher.invoke(trigger) + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { mainFetcher.invoke(trigger) } + } + + @Test + fun `does nothing when trigger currency is not present in any account`() = runTest { + val utils = createUtils() + // main account without the triggered currency + val accountListFlow = MutableStateFlow(accountListOf(Account.CryptoPortfolio.createMainAccount(WALLET_ID))) + every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow + val mainFetcher = relaxedAccountFetcher() + every { accountFetcherFactory.create(any()) } returns mainFetcher + + val fetcher = createFetcher(utils) + advanceUntilIdle() + + // Act + val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID, currency = currency) + val result = fetcher.invoke(trigger) + advanceUntilIdle() + + // Assert + coVerify(inverse = true) { mainFetcher.invoke(any()) } + } + + @Test + fun `close cancels scope and closes all child fetchers`() = runTest { + val utils = createUtils() + val accountListFlow = MutableStateFlow(accountListOf(mainAccount, secondAccount)) + every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow + val mainFetcher = relaxedAccountFetcher() + val secondFetcher = relaxedAccountFetcher() + every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher + every { accountFetcherFactory.create(secondAccount.accountId) } returns secondFetcher + + val fetcher = createFetcher(utils) + advanceUntilIdle() + assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId, secondAccount.accountId) + + // Act + fetcher.close() + + // Assert + assertThat(fetcher.fetchers).isEmpty() + verify(exactly = 1) { mainFetcher.close() } + verify(exactly = 1) { secondFetcher.close() } + assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse() + } + + private fun accountListOf(vararg accounts: Account): AccountList = AccountList( + userWalletId = WALLET_ID, + accounts = accounts.toList(), + totalAccounts = accounts.size, + totalArchivedAccounts = 0, + ).getOrNull()!! + + private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils( + appScope = TestAppCoroutineScope(testScope = this), + analyticsEventHandler = mockk(relaxed = true), + analyticsExceptionHandler = mockk(relaxed = true), + ) + + private fun createFetcher(utils: DefaultTxHistoryFetcherUtils) = DefaultWalletTxHistoryFetcher( + walletId = WALLET_ID, + utils = utils, + singleAccountListSupplier = singleAccountListSupplier, + accountTxHistoryFetcher = accountFetcherFactory, + ) + + private fun relaxedAccountFetcher() = mockk(relaxed = true) + + private companion object { + val WALLET_ID: UserWalletId = MockAccounts.userWalletId + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt index a8a0e5cdbb..dbd4b4612b 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt @@ -19,6 +19,10 @@ abstract class SingleAccountSupplier( override val keyCreator: (SingleAccountProducer.Params) -> String, ) : FlowCachingSupplier() { + operator fun invoke(accountId: AccountId): Flow { + return invoke(params = SingleAccountProducer.Params(accountId)) + } + fun filterPaymentAccount(accountId: AccountId): Flow { return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() } diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/fetcher/TxHistoryFetchTrigger.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/fetcher/TxHistoryFetchTrigger.kt new file mode 100644 index 0000000000..a94ce532f0 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/fetcher/TxHistoryFetchTrigger.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.txhistory.fetcher + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId + +sealed interface TxHistoryFetchTrigger { + + data class TokenDetailsOpen( + val walletId: UserWalletId, + val currency: CryptoCurrency, + ) : TxHistoryFetchTrigger, TxHistoryExpressTrigger, TxHistoryGatewayTrigger + + data class TokenDetailsPTR( + val walletId: UserWalletId, + val currency: CryptoCurrency, + ) : TxHistoryFetchTrigger, TxHistoryExpressTrigger, TxHistoryGatewayTrigger +} + +sealed interface TxHistoryExpressTrigger : TxHistoryFetchTrigger +sealed interface TxHistoryGatewayTrigger : TxHistoryFetchTrigger \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/fetcher/TxHistoryFetcher.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/fetcher/TxHistoryFetcher.kt new file mode 100644 index 0000000000..6c2e174a9b --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/fetcher/TxHistoryFetcher.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.txhistory.fetcher + +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId + +interface TxHistoryFetcher { + suspend fun invoke(params: T) + fun close() +} + +interface AppTxHistoryFetcher : TxHistoryFetcher + +interface WalletTxHistoryFetcher : TxHistoryFetcher { + val walletId: UserWalletId +} + +interface AccountTxHistoryFetcher : TxHistoryFetcher { + val accountId: AccountId + val walletId: UserWalletId get() = accountId.userWalletId +} + +interface ExpressTxHistoryFetcher : TxHistoryFetcher { + val address: String +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt index acc42bd153..211cf58751 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Option import arrow.core.none import arrow.core.some import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -21,11 +22,7 @@ class GetPaymentAccountCryptoCurrencyStatusUseCase( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): Flow> { - return paymentAccountStatusSupplier(userWalletId).mapNotNull { accountStatus -> - val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) { - is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus - else -> return@mapNotNull null - } + return invoke(userWalletId).mapNotNull { (accountStatus, cryptoCurrencyStatus) -> if (cryptoCurrencyStatus.currency == cryptoCurrency) { accountStatus.account to cryptoCurrencyStatus } else { @@ -34,6 +31,16 @@ class GetPaymentAccountCryptoCurrencyStatusUseCase( } } + operator fun invoke(userWalletId: UserWalletId): Flow> { + return paymentAccountStatusSupplier(userWalletId).mapNotNull { accountStatus -> + val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) { + is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus + else -> return@mapNotNull null + } + accountStatus to cryptoCurrencyStatus + } + } + suspend fun invokeSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index b5506dacff..19a253810c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -27,6 +27,10 @@ class GetSelectedWalletUseCase( } } + fun selectedFlow(): Flow { + return userWalletsListRepository.selectedUserWallet.filterNotNull() + } + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") fun sync(): Either { return either { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 355ce124c1..3db4bbf7e6 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -22,13 +23,14 @@ class GetWalletsUseCase( operator fun invoke(): Flow> = userWalletsListRepository.userWallets.map { requireNotNull(it) } @Throws(IllegalArgumentException::class) - fun invokeAsMap(isOnlyMultiCurrency: Boolean = true): Flow> = invoke() + fun invokeAsMap( + isOnlyMultiCurrency: Boolean = true, + filterLocked: Boolean = false, + ): Flow> = invoke() .map { list -> - val wallets = if (isOnlyMultiCurrency) { - list.filter { wallet -> wallet.isMultiCurrency } - } else { - list - } + val wallets = list + .filter { wallet -> if (isOnlyMultiCurrency) wallet.isMultiCurrency else true } + .filter { wallet -> if (filterLocked) !wallet.isLocked else true } wallets.associateByTo( destination = linkedMapOf(), keySelector = { wallet -> wallet.walletId }, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt index 8b1670bc5d..7748f84f93 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt @@ -4,7 +4,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -61,8 +60,7 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( } private fun buildFlow() = flow { - val walletsFlow = getWalletsUseCase.invokeAsMap() - .map { wallets -> wallets.filterNot { (_, wallet) -> wallet.isLocked } } + val walletsFlow = getWalletsUseCase.invokeAsMap(filterLocked = true) val fullPortfolioBlockFlow = combine( flow = walletsFlow, flow2 = portfolioListBlockDelegate.portfolioList,