From e96b780d21af801ba6bfb71540658f57716f2ee0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 11:07:06 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../local/network/entity/NetworkStatusDM.kt | 33 ++ .../multi/DefaultMultiNetworkStatusFetcher.kt | 88 +-- .../DefaultSingleNetworkStatusFetcher.kt | 58 +- .../DefaultMultiNetworkStatusFetcherTest.kt | 518 ++++++++++++++---- .../DefaultSingleNetworkStatusFetcherTest.kt | 122 +++-- .../multi/MultiNetworkStatusFetcher.kt | 2 +- .../single/SingleNetworkStatusFetcher.kt | 22 +- .../tokens/FetchCurrencyStatusUseCase.kt | 2 +- .../UpdateDelayedNetworkStatusUseCase.kt | 2 +- .../usecase/SendTransactionUseCase.kt | 2 +- 10 files changed, 577 insertions(+), 272 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index 40f26e9e08..de66806e7f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -2,17 +2,40 @@ package com.tangem.datasource.local.network.entity import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount +import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified import com.tangem.domain.tokens.model.Network import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel import java.math.BigDecimal +/** + * Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount]. + * + * @see [com.tangem.domain.tokens.model.NetworkStatus] + */ sealed interface NetworkStatusDM { + /** Network id */ val networkId: Network.ID + + /** Derivation path */ val derivationPath: DerivationPath + + /** Selected address */ val selectedAddress: String + + /** Available address */ val availableAddresses: Set
+ /** + * Verified + * + * @property networkId network id + * @property derivationPath derivation path + * @property selectedAddress selected address + * @property availableAddresses available addresses + * @property amounts amounts + */ @NameLabel("amounts") data class Verified( @Json(name = "network_id") override val networkId: Network.ID, @@ -22,6 +45,16 @@ sealed interface NetworkStatusDM { @Json(name = "amounts") val amounts: Map, ) : NetworkStatusDM + /** + * No account + * + * @property networkId network id + * @property derivationPath derivation path + * @property selectedAddress selected address + * @property availableAddresses available addresses + * @property amountToCreateAccount amount to create account + * @property errorMessage error message + */ @NameLabel("amount_to_create_account") data class NoAccount( @Json(name = "network_id") override val networkId: Network.ID, diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index 67a3ba3d44..d696addbdb 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -2,26 +2,15 @@ package com.tangem.data.networks.multi import arrow.core.raise.catch import arrow.core.raise.ensure -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsOnlyCache -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -31,28 +20,24 @@ import javax.inject.Inject /** * Default implementation of [MultiNetworkStatusFetcher] * - * @property singleNetworkStatusFetcher single network status fetcher * @property networksStatusesStore networks statuses store + * @property userWalletsStore user wallets store + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property commonNetworkStatusFetcher common network status fetcher + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( - excludedBlockchains: ExcludedBlockchains, private val networksStatusesStore: NetworksStatusesStoreV2, private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, - private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val dispatchers: CoroutineDispatcherProvider, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, + private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusFetcher { - private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) } - override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { - // Optimization! - // Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store. - // So if we update all networks at once, it will be more efficient. networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks) val userWallet = catch( @@ -67,27 +52,39 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( }, ) - val isNotSingleWallet = with(userWallet.scanResponse.cardTypesResolver) { + val cardTypesResolver = userWallet.cardTypesResolver + val isWalletSupported = with(cardTypesResolver) { isMultiwalletAllowed() || isSingleWalletWithToken() } - ensure(isNotSingleWallet) { - networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, networks = params.networks) + ensure(isWalletSupported) { + networksStatusesStore.setSourceAsOnlyCache( + userWalletId = params.userWalletId, + networks = params.networks, + ) IllegalStateException("User wallet is not multi-currency") } - val networksCurrencies = createCurrencies(userWallet = userWallet, networks = params.networks) + val networksCurrencies = if (cardTypesResolver.isMultiwalletAllowed()) { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard( + userWallet = userWallet, + networks = params.networks, + ) + } else { + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, + ) + .groupBy { it.network } + } val result = coroutineScope { params.networks .map { network -> async { - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Prepared( - userWalletId = params.userWalletId, - network = network, - addedNetworkCurrencies = networksCurrencies.filter { it.network == network }.toSet(), - ), + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = network, + networkCurrencies = networksCurrencies[network].orEmpty().toSet(), ) } } @@ -100,31 +97,4 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( IllegalStateException("Failed to fetch network statuses") } } - - private suspend fun createCurrencies(userWallet: UserWallet, networks: Set): Set { - val blockchains = networks.map { Blockchain.fromNetworkId(networkId = it.backendId) } - - // multi-currency wallet - if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, networks = networks) - - // check if the blockchain of single-currency wallet is the same as network - val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - if (!blockchains.contains(cardBlockchain)) return emptySet() - - return cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() - } - - private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set): Set { - val response = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), - ) ?: return emptySet() - - return responseCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { token -> - networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } - }, - scanResponse = userWallet.scanResponse, - ) - .toSet() - } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index ef23a8c4fc..c3a38ceb1b 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -2,70 +2,46 @@ package com.tangem.data.networks.single import arrow.core.Either import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.setSourceAsCache -import com.tangem.data.networks.store.setSourceAsOnlyCache -import com.tangem.data.networks.store.storeStatus -import com.tangem.data.networks.utils.NetworkStatusFactory import com.tangem.domain.core.utils.catchOn import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject /** * Default implementation of [SingleNetworkStatusFetcher] * - * @property walletManagersFacade wallet managers facade - * @property networksStatusesStore networks statuses store - * @property cardCryptoCurrencyFactory card crypto currency factory - * @property dispatchers dispatchers + * @property commonNetworkStatusFetcher common network status fetcher + * @property networksStatusesStore networks statuses store + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( - private val walletManagersFacade: WalletManagersFacade, + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, private val networksStatusesStore: NetworksStatusesStoreV2, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleNetworkStatusFetcher { - override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { - val networkCurrencies = when (params) { - is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies - is SingleNetworkStatusFetcher.Params.Simple -> { - networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) + override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either { + return Either.catchOn(dispatchers.default) { + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) - cardCryptoCurrencyFactory.create( - userWalletId = params.userWalletId, - network = params.network, - ) - } - } - - val result = withContext(dispatchers.io) { - walletManagersFacade.update( + val networkCurrencies = cardCryptoCurrencyFactory.create( userWalletId = params.userWalletId, network = params.network, - extraTokens = networkCurrencies - .filterIsInstance() - .toSet(), ) + + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = networkCurrencies.toSet(), + ) + .onLeft { throw it } } - - val status = NetworkStatusFactory.create( - network = params.network, - updatingResult = result, - addedCurrencies = networkCurrencies.toSet(), - ) - - networksStatusesStore.storeStatus(userWalletId = params.userWalletId, status = status) } - .onLeft { - Timber.e("Failed to fetch network status for $params: $it") - networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network) - } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index 18ea008ac3..069df070d9 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -1,119 +1,409 @@ package com.tangem.data.networks.multi +import arrow.core.Either +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsCache +import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + /** [REDACTED_AUTHOR] */ -// internal class DefaultMultiNetworkStatusFetcherTest { -// -// private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() -// private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) -// -// private val fetcher = DefaultMultiNetworkStatusFetcher( -// singleNetworkStatusFetcher = singleNetworkStatusFetcher, -// networksStatusesStore = networksStatusesStore, -// ) -// -// @Test -// fun `fetch networks statuses successfully`() = runTest { -// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) -// -// val ethParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.first(), -// applyRefresh = false, -// ) -// -// val stellarParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.last(), -// applyRefresh = false, -// ) -// -// coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() -// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() -// -// val actual = fetcher(params) -// -// coVerify { -// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) -// singleNetworkStatusFetcher(ethParams) -// singleNetworkStatusFetcher(stellarParams) -// } -// -// Truth.assertThat(actual.isRight()).isTrue() -// } -// -// @Test -// fun `fetch networks statuses failure if one of them fails`() = runTest { -// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) -// -// val ethParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.first(), -// applyRefresh = false, -// ) -// -// val stellarParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.last(), -// applyRefresh = false, -// ) -// -// val ethException = IllegalStateException("eth") -// coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() -// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() -// -// val actual = fetcher(params) -// -// coVerify { -// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) -// singleNetworkStatusFetcher(ethParams) -// singleNetworkStatusFetcher(stellarParams) -// } -// -// val expected = IllegalStateException("Failed to fetch network statuses") -// -// Truth.assertThat(actual.isLeft()).isTrue() -// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) -// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) -// } -// -// @Test -// fun `fetch networks statuses failure if all of them fails`() = runTest { -// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) -// -// val ethParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.first(), -// applyRefresh = false, -// ) -// -// val stellarParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.last(), -// applyRefresh = false, -// ) -// -// coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() -// coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() -// -// val actual = fetcher(params) -// -// coVerify { -// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) -// singleNetworkStatusFetcher(ethParams) -// singleNetworkStatusFetcher(stellarParams) -// } -// -// val expected = IllegalStateException("Failed to fetch network statuses") -// -// Truth.assertThat(actual.isLeft()).isTrue() -// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) -// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) -// } -// -// private companion object { -// val userWalletId = UserWalletId("011") -// val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet() -// } -// } \ No newline at end of file +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultMultiNetworkStatusFetcherTest { + + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + private val userWalletsStore: UserWalletsStore = mockk() + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() + + private val fetcher = DefaultMultiNetworkStatusFetcher( + networksStatusesStore = networksStatusesStore, + userWalletsStore = userWalletsStore, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + commonNetworkStatusFetcher = commonNetworkStatusFetcher, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(networksStatusesStore, userWalletsStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher) + } + + @Test + fun `fetch successfully for multi-currency card`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = mapOf( + ethereum.network to listOf(ethereum), + cardano.network to listOf(cardano), + ) + val ethereumFetcherResult = Either.Right(Unit) + val cardanoFetcherResult = Either.Right(Unit) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } returns cardanoFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Right(Unit) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + } + } + + @Test + fun `fetch successfully for single-currency card with token`() = runTest { + // Arrange + val networks = setOf(ethereum.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = listOf(ethereum) + val ethereumFetcherResult = Either.Right(Unit) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false + coEvery { cardTypesResolver.isSingleWalletWithToken() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Right(Unit) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) + } + } + + @Test + fun `fetch failure if one of them fails`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = mapOf( + ethereum.network to listOf(ethereum), + cardano.network to listOf(cardano), + ) + val ethereumFetcherResult = Either.Left(IllegalStateException()) + val cardanoFetcherResult = Either.Right(Unit) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } returns cardanoFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(IllegalStateException("Failed to fetch network statuses")) + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + } + } + + @Test + fun `fetch failure if all of them fails`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = mapOf( + ethereum.network to listOf(ethereum), + cardano.network to listOf(cardano), + ) + val ethereumFetcherResult = Either.Left(IllegalStateException("ethereum")) + val cardanoFetcherResult = Either.Left(IllegalStateException("cardano")) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } returns cardanoFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(IllegalStateException("Failed to fetch network statuses")) + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + } + } + + @Test + fun `fetch failure if userWalletsStore throws exception`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWalletStoreException = IllegalStateException() + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } throws userWalletStoreException + coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(userWalletStoreException) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) + } + + coVerify(inverse = true) { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + commonNetworkStatusFetcher.fetch(any(), any(), any()) + } + } + + @Test + fun `fetch failure if card is single-currency`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false + coEvery { cardTypesResolver.isSingleWalletWithToken() } returns false + + coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(IllegalStateException("User wallet is not multi-currency")) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardTypesResolver.isSingleWalletWithToken() + networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + commonNetworkStatusFetcher.fetch(any(), any(), any()) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + val ethereum = cryptoCurrencyFactory.ethereum + val cardano = cryptoCurrencyFactory.cardano + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index 813f2d20c1..21b5a1df8d 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -1,97 +1,141 @@ package com.tangem.data.networks.single +import arrow.core.Either +import arrow.core.left import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.setSourceAsCache -import com.tangem.data.networks.store.storeSuccess import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultSingleNetworkStatusFetcherTest { - private val walletManagersFacade: WalletManagersFacade = mockk(relaxUnitFun = true) + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val fetcher = DefaultSingleNetworkStatusFetcher( - walletManagersFacade = walletManagersFacade, + commonNetworkStatusFetcher = commonNetworkStatusFetcher, networksStatusesStore = networksStatusesStore, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) + @BeforeEach + fun resetMocks() { + clearMocks(commonNetworkStatusFetcher, networksStatusesStore, cardCryptoCurrencyFactory) + } + @Test - fun `fetch network status successfully`() = runTest { - val params = createParams() + fun `fetch successfully`() = runTest { + // Arrange + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) + val networkCurrencies = listOf(ethereum) + val commonFetcherResult = Either.Right(Unit) - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) - - val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = networkCurrencies.toSet(), + ) + } returns commonFetcherResult + // Act val actual = fetcher(params) + // Assert + val expected = commonFetcherResult + + Truth.assertThat(actual).isEqualTo(expected) + coVerifyOrder { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) cardCryptoCurrencyFactory.create(params.userWalletId, params.network) - walletManagersFacade.update(params.userWalletId, params.network, emptySet()) - networksStatusesStore.storeSuccess( - userWalletId = params.userWalletId, - status = NetworkStatus(params.network, NetworkStatus.MissedDerivation), - ) + commonNetworkStatusFetcher.fetch(params.userWalletId, params.network, setOf(ethereum)) } - - Truth.assertThat(actual.isRight()).isTrue() } @Test - fun `fetch network status failure`() = runTest { - val params = createParams() + fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest { + // Arrange + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) + val factoryException = IllegalStateException() - val exception = IllegalStateException() - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws exception + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws factoryException + // Act val actual = fetcher(params) + // Arrange + val expected = Either.Left(factoryException) + Truth.assertThat(actual).isEqualTo(expected) + coVerifyOrder { networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) - // networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network) } coVerify(inverse = true) { - walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any()) - // networksStatusesStore.storeSuccess(userWalletId = any(), status = any()) + commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any()) } - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isEqualTo(exception) } - private fun createParams(): SingleNetworkStatusFetcher.Params { - return SingleNetworkStatusFetcher.Params.Simple( - userWalletId = UserWalletId("011"), - network = ethereum.network, - ) + @Test + fun `fetch failure if commonNetworkStatusFetcher returns exception`() = runTest { + // Arrange + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) + val networkCurrencies = listOf(ethereum) + val commonFetcherResult = IllegalStateException().left() + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = networkCurrencies.toSet(), + ) + } returns commonFetcherResult + + // Act + val actual = fetcher(params) + + // Arrange + val expected = commonFetcherResult + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) + cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = setOf(ethereum), + ) + } } private companion object { + val userWalletId = UserWalletId("011") val ethereum = MockCryptoCurrencyFactory().ethereum } } \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt index 0710aa80d3..c1afcf6834 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt @@ -5,7 +5,7 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId /** - * Fetcher of network status [Network] for wallet with [UserWalletId] + * Fetcher of network status [Network] for multi-currency wallet with [UserWalletId] * [REDACTED_AUTHOR] */ diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt index 30836055f1..3ad9457bea 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt @@ -1,7 +1,6 @@ package com.tangem.domain.networks.single import com.tangem.domain.core.flow.FlowFetcher -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -12,18 +11,11 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface SingleNetworkStatusFetcher : FlowFetcher { - /** Params */ - sealed interface Params { - - val userWalletId: UserWalletId - val network: Network - - data class Simple(override val userWalletId: UserWalletId, override val network: Network) : Params - - data class Prepared( - override val userWalletId: UserWalletId, - override val network: Network, - val addedNetworkCurrencies: Set, - ) : Params - } + /** + * Params + * + * @property userWalletId user wallet id + * @property network network + */ + data class Params(val userWalletId: UserWalletId, val network: Network) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 303758d938..dee7078730 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -119,7 +119,7 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), ) .mapLeft { CurrencyStatusError.DataError(it) } .bind() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index ada4cab157..632b102018 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -38,7 +38,7 @@ class UpdateDelayedNetworkStatusUseCase( private suspend fun Raise.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), ) .mapLeft(CurrencyStatusError::DataError) .bind() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 0bf8d57527..e519d17ac5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -82,7 +82,7 @@ class SendTransactionUseCase( return sendResult .onRight { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Simple( + params = SingleNetworkStatusFetcher.Params( userWalletId = userWallet.walletId, network = network, ),