Updated on 2026-08-14
This commit is contained in:
parent
642190f7c6
commit
3a5144403b
19 changed files with 1033 additions and 49 deletions
|
|
@ -9,12 +9,17 @@ android {
|
|||
namespace = "com.tangem.data.txhistory"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<String, ExpressTxHistoryFetcher>()
|
||||
|
||||
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<Unit> = channelFlow {
|
||||
val accountFlow = singleAccountSupplier(accountId).stateIn(this)
|
||||
|
||||
val controlFetchersFlow = when (accountFlow.value) {
|
||||
is Account.CryptoPortfolio -> accountFlow
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.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<Unit> {
|
||||
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<Account.CryptoPortfolio>.controlFetchersForCryptoAccount(): Flow<Unit> {
|
||||
return map { account -> account.cryptoCurrencies }
|
||||
.map { currencies ->
|
||||
val onlyCoins = currencies.filterIsInstance<CryptoCurrency.Coin>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWalletId, WalletTxHistoryFetcher>()
|
||||
|
||||
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<Unit> = channelFlow {
|
||||
val walletsFlow: StateFlow<Map<UserWalletId, UserWallet>> = 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<Set<UserWalletId>>.createForNewWallets() =
|
||||
onEach { ids -> ids.forEach { walletId -> getOrPutFetcher(walletId) } }
|
||||
|
||||
private fun Flow<Set<UserWalletId>>.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AccountId, AccountTxHistoryFetcher>()
|
||||
|
||||
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<Unit> = 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<Set<AccountId>>.createForNewAccounts() =
|
||||
onEach { ids -> ids.forEach { id -> getOrPutFetcher(id) } }
|
||||
|
||||
private fun Flow<Set<AccountId>>.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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TxHistoryFetchTrigger>
|
||||
|
||||
val fetcherScope: CoroutineScope
|
||||
val analyticsEventHandler: AnalyticsEventHandler
|
||||
val analyticsExceptionHandler: AnalyticsExceptionHandler
|
||||
|
||||
suspend fun sendTrigger(trigger: TxHistoryFetchTrigger)
|
||||
|
||||
companion object {
|
||||
|
||||
fun TxHistoryFetcherUtils.cancelScope() = fetcherScope.cancel()
|
||||
|
||||
fun <T> TxHistoryFetcherUtils.defaultLaunchIn(flow: Flow<T>) = flow
|
||||
.retry { error ->
|
||||
logError(error)
|
||||
true
|
||||
}
|
||||
.launchIn(fetcherScope)
|
||||
|
||||
fun TxHistoryFetcherUtils.receiveTrigger(): Flow<TxHistoryFetchTrigger> {
|
||||
return triggersBuffer.receiveAsFlow()
|
||||
}
|
||||
|
||||
inline fun <reified R> TxHistoryFetcherUtils.receiveTriggerInstance(): Flow<R> {
|
||||
return receiveTrigger().filterIsInstance<R>()
|
||||
}
|
||||
|
||||
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<TxHistoryFetchTrigger> = Channel(Channel.BUFFERED)
|
||||
|
||||
// todo txhistory use lifecycle scope?
|
||||
override val fetcherScope: CoroutineScope = appScope + SupervisorJob()
|
||||
|
||||
override suspend fun sendTrigger(trigger: TxHistoryFetchTrigger) {
|
||||
triggersBuffer.trySend(trigger)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Account>(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<Account>(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<Account>(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>(Account.Payment(WALLET_ID))
|
||||
every { singleAccountSupplier.invoke(paymentAccountId) } returns accountFlow
|
||||
|
||||
val paymentStatus = mockk<AccountStatus.Payment>(relaxed = true)
|
||||
val currencyStatus = mockk<CryptoCurrencyStatus> { 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<Account>(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<DefaultExpressTxHistoryFetcher>(relaxed = true)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID = MockAccounts.userWalletId
|
||||
const val ADDRESS = "0xEthAddress"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWalletId, UserWallet>())
|
||||
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<UserWalletId, UserWallet>(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<UserWalletId, UserWallet>(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<UserWalletId, UserWallet>())
|
||||
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<UserWalletId, UserWallet>(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<DefaultWalletTxHistoryFetcher>(relaxed = true)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID_1 = UserWalletId("001")
|
||||
val WALLET_ID_2 = UserWalletId("002")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DefaultAccountTxHistoryFetcher>(relaxed = true)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID: UserWalletId = MockAccounts.userWalletId
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,10 @@ abstract class SingleAccountSupplier(
|
|||
override val keyCreator: (SingleAccountProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<SingleAccountProducer, SingleAccountProducer.Params, Account>() {
|
||||
|
||||
operator fun invoke(accountId: AccountId): Flow<Account> {
|
||||
return invoke(params = SingleAccountProducer.Params(accountId))
|
||||
}
|
||||
|
||||
fun filterPaymentAccount(accountId: AccountId): Flow<Account.Payment> {
|
||||
return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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<T : TxHistoryFetchTrigger> {
|
||||
suspend fun invoke(params: T)
|
||||
fun close()
|
||||
}
|
||||
|
||||
interface AppTxHistoryFetcher : TxHistoryFetcher<TxHistoryFetchTrigger>
|
||||
|
||||
interface WalletTxHistoryFetcher : TxHistoryFetcher<TxHistoryFetchTrigger> {
|
||||
val walletId: UserWalletId
|
||||
}
|
||||
|
||||
interface AccountTxHistoryFetcher : TxHistoryFetcher<TxHistoryFetchTrigger> {
|
||||
val accountId: AccountId
|
||||
val walletId: UserWalletId get() = accountId.userWalletId
|
||||
}
|
||||
|
||||
interface ExpressTxHistoryFetcher : TxHistoryFetcher<TxHistoryExpressTrigger> {
|
||||
val address: String
|
||||
}
|
||||
|
|
@ -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<Pair<Account.Payment, CryptoCurrencyStatus>> {
|
||||
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<Pair<AccountStatus.Payment, CryptoCurrencyStatus>> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ class GetSelectedWalletUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
fun selectedFlow(): Flow<UserWallet> {
|
||||
return userWalletsListRepository.selectedUserWallet.filterNotNull()
|
||||
}
|
||||
|
||||
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
|
||||
fun sync(): Either<GetUserWalletError, UserWallet?> {
|
||||
return either {
|
||||
|
|
|
|||
|
|
@ -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<List<UserWallet>> = userWalletsListRepository.userWallets.map { requireNotNull(it) }
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun invokeAsMap(isOnlyMultiCurrency: Boolean = true): Flow<LinkedHashMap<UserWalletId, UserWallet>> = invoke()
|
||||
fun invokeAsMap(
|
||||
isOnlyMultiCurrency: Boolean = true,
|
||||
filterLocked: Boolean = false,
|
||||
): Flow<LinkedHashMap<UserWalletId, UserWallet>> = 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 },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue