diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 24e9f844e5..a593d255a9 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -31,6 +31,14 @@ class ResponseCryptoCurrenciesFactory( .toList() } + fun createCurrencies(tokens: List, scanResponse: ScanResponse): List { + return tokens + .asSequence() + .mapNotNull { createCurrency(it, scanResponse) } + .distinctBy(CryptoCurrency::id) + .toList() + } + fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { 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 new file mode 100644 index 0000000000..71d51a5793 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -0,0 +1,46 @@ +package com.tangem.data.networks.multi + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +/** + * Default implementation of [MultiNetworkStatusFetcher] + * + * @property singleNetworkStatusFetcher single network status fetcher + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiNetworkStatusFetcher @Inject constructor( + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, +) : MultiNetworkStatusFetcher { + + override suspend fun invoke(params: MultiNetworkStatusFetcher.Params): Either = either { + val result = coroutineScope { + params.networks + .map { + async { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = params.userWalletId, + network = it, + ), + ) + } + } + .awaitAll() + } + + val failedResult = result.firstOrNull { it.isLeft() } + + ensure(failedResult == null) { + IllegalStateException("Failed to fetch network statuses") + } + } +} \ 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 new file mode 100644 index 0000000000..9da7afbce4 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -0,0 +1,114 @@ +package com.tangem.data.networks.single + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory +import com.tangem.data.tokens.utils.NetworkStatusFactory +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.demo.DemoConfig +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.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +/** + * Default implementation of [SingleNetworkStatusFetcher] + * + * @param excludedBlockchains excluded blockchains + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property userWalletsStore user wallets store + * @property appPreferencesStore app preferences store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleNetworkStatusFetcher @Inject constructor( + excludedBlockchains: ExcludedBlockchains, + private val walletManagersFacade: WalletManagersFacade, + private val networksStatusesStore: NetworksStatusesStoreV2, + private val userWalletsStore: UserWalletsStore, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SingleNetworkStatusFetcher { + + private val demoConfig = DemoConfig() + private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) + private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + private val networkStatusFactory = NetworkStatusFactory() + + override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either = Either.catch { + networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network) + + val result = withContext(dispatchers.io) { + walletManagersFacade.update( + userWalletId = params.userWalletId, + network = params.network, + extraTokens = networkCurrencies + .filterIsInstance() + .toSet(), + ) + } + + val status = networkStatusFactory.createNetworkStatus( + network = params.network, + result = result, + currencies = networkCurrencies.toSet(), + ) + + networksStatusesStore.storeActual(userWalletId = params.userWalletId, value = status) + } + .onLeft { + Timber.e("Failed to fetch network status for $params: $it") + networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) + } + + private suspend fun createCurrencies(userWallet: UserWallet, network: Network): List { + val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + + // multi-currency wallet + if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) + + // check if the blockchain of single-currency wallet is the same as network + val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + if (cardBlockchain != blockchain) return emptyList() + + // single-currency wallet with token (NODL) + if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } + + // single-currency wallet + return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) + } + + private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { + val response = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), + ) ?: return emptyList() + + return responseCurrenciesFactory.createCurrencies( + tokens = response.tokens.filter { + it.networkId == network.backendId && it.derivationPath == network.derivationPath.value + }, + scanResponse = userWallet.scanResponse, + ) + } +} \ 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 new file mode 100644 index 0000000000..f74995432d --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -0,0 +1,119 @@ +package com.tangem.data.networks.multi + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.wallets.models.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiNetworkStatusFetcherTest { + + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() + + private val fetcher = DefaultMultiNetworkStatusFetcher(singleNetworkStatusFetcher = singleNetworkStatusFetcher) + + @Test + fun `fetch networks statuses successfully`() = runTest { + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) + + val ethParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.first(), + ) + + val stellarParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.last(), + ) + + coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() + coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() + + val actual = fetcher(params) + + coVerify { + 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(), + ) + + val stellarParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.last(), + ) + + val ethException = IllegalStateException("eth") + coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() + coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() + + val actual = fetcher(params) + + coVerify { + 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(), + ) + + val stellarParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.last(), + ) + + coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() + coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() + + val actual = fetcher(params) + + coVerify { + 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 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 new file mode 100644 index 0000000000..b448cddeec --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -0,0 +1,88 @@ +package com.tangem.data.networks.single + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.datasource.local.userwallet.UserWalletsStore +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.Ordering +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultSingleNetworkStatusFetcherTest { + + private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true) + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) + private val userWalletsStore: UserWalletsStore = mockk(relaxed = true) + + private val fetcher = DefaultSingleNetworkStatusFetcher( + excludedBlockchains = mockk(relaxed = true), + walletManagersFacade = walletManagersFacade, + networksStatusesStore = networksStatusesStore, + userWalletsStore = userWalletsStore, + appPreferencesStore = mockk(relaxed = true), + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `fetch network status successfully`() = runTest { + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network) + + val result = UpdateWalletManagerResult.MissedDerivation + coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + + val actual = fetcher(params) + + coVerify(ordering = Ordering.SEQUENCE) { + networksStatusesStore.refresh(userWalletId = userWalletId, network = network) + userWalletsStore.getSyncStrict(key = userWalletId) + walletManagersFacade.update(userWalletId, network, emptySet()) + networksStatusesStore.storeActual( + userWalletId = userWalletId, + value = NetworkStatus(network, NetworkStatus.MissedDerivation), + ) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch network status failure`() = runTest { + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network) + + val exception = IllegalStateException() + coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception + + val actual = fetcher(params) + + coVerify(ordering = Ordering.SEQUENCE) { + networksStatusesStore.refresh(userWalletId = userWalletId, network = network) + userWalletsStore.getSyncStrict(key = userWalletId) + networksStatusesStore.storeError(userWalletId = userWalletId, network = network) + } + + coVerify(inverse = true) { + walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any()) + networksStatusesStore.storeActual(userWalletId = any(), value = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isEqualTo(exception) + } + + private companion object { + val userWalletId = UserWalletId("011") + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 3ccfb526a8..5f781ffb46 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -9,7 +9,7 @@ import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber -internal class NetworkStatusFactory { +class NetworkStatusFactory { fun createNetworkStatus( network: Network, 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 new file mode 100644 index 0000000000..0710aa80d3 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.networks.multi + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Fetcher of network status [Network] for wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiNetworkStatusFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId, val networks: Set) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..791e66ea05 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.networks.single + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Fetcher of network status [Network] for wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface SingleNetworkStatusFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId, val network: Network) +} \ No newline at end of file