diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt new file mode 100644 index 0000000000..fcb4cdd145 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -0,0 +1,131 @@ +package com.tangem.data.tokens + +import arrow.core.Either +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.core.utils.catchOn +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.isMultiCurrency +import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Default implementation of [MultiWalletCryptoCurrenciesFetcher] + * + * @property userWalletsStore [UserWallet]'s store + * @property tangemTechApi Tangem Tech API + * @property userTokensResponseStore store of [UserTokensResponse] + * @property userTokensSaver user tokens saver + * @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card + * @property expressServiceLoader express service loader + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Suppress("LongParameterList") +internal class DefaultMultiWalletCryptoCurrenciesFetcher( + private val demoConfig: DemoConfig, + private val userWalletsStore: UserWalletsStore, + private val tangemTechApi: TangemTechApi, + private val customTokensMerger: CustomTokensMerger, + private val userTokensResponseStore: UserTokensResponseStore, + private val userTokensSaver: UserTokensSaver, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val expressServiceLoader: ExpressServiceLoader, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiWalletCryptoCurrenciesFetcher { + + private val userTokensResponseFactory = UserTokensResponseFactory() + + override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + + if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") + + val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) { + createDefaultUserTokensResponse(scanResponse = userWallet.scanResponse) + } else { + safeApiCall( + call = { + withContext(dispatchers.io) { + tangemTechApi.getUserTokens(userId = userWallet.walletId.stringValue).bind() + } + }, + onError = { + handleFetchTokensError(error = it, userWallet = userWallet.requireColdWallet()) // TODO 11142 + }, + ) + } + + val compatibleUserTokensResponse = response + .let { it.copy(tokens = it.tokens.distinct()) } + .let { customTokensMerger.mergeIfPresented(userWalletId = userWallet.walletId, response = it) } + + userTokensSaver.store(userWalletId = userWallet.walletId, response = compatibleUserTokensResponse) + + fetchExpressAssetsByNetworkIds(userWallet = userWallet, userTokens = compatibleUserTokensResponse) + } + + private suspend fun UserWallet.Cold.isDemoWalletWithoutSavedTokens(): Boolean { + val isDemoCard = demoConfig.isDemoCardId(cardId = cardId) + + return if (isDemoCard) { + val response = userTokensResponseStore.getSyncOrNull(userWalletId = walletId) + + response == null + } else { + false + } + } + + private suspend fun handleFetchTokensError(error: ApiResponseError, userWallet: UserWallet): UserTokensResponse { + val userWalletId = userWallet.walletId + + val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + ?: createDefaultUserTokensResponse(scanResponse = userWallet.requireColdWallet().scanResponse) + + if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) { + Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId") + + userTokensSaver.push(userWalletId, response) + } + + return response + } + + private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { + val tokens = userTokens.tokens.map { token -> + LeastTokenInfo( + contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = token.networkId, + ) + } + + expressServiceLoader.update(userWallet = userWallet, userTokens = tokens) + } + + private fun createDefaultUserTokensResponse(scanResponse: ScanResponse): UserTokensResponse { + return userTokensResponseFactory.createUserTokensResponse( + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt new file mode 100644 index 0000000000..93689fc896 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt @@ -0,0 +1,51 @@ +package com.tangem.data.tokens.di + +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class MultiWalletCryptoCurrenciesFetcherModule { + + @Singleton + @Provides + fun provideMultiWalletCryptoCurrenciesFetcher( + tangemTechApi: TangemTechApi, + userWalletsStore: UserWalletsStore, + userTokensResponseStore: UserTokensResponseStore, + userTokensSaver: UserTokensSaver, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + expressServiceLoader: ExpressServiceLoader, + dispatchers: CoroutineDispatcherProvider, + ): MultiWalletCryptoCurrenciesFetcher { + return DefaultMultiWalletCryptoCurrenciesFetcher( + demoConfig = DemoConfig(), + userWalletsStore = userWalletsStore, + tangemTechApi = tangemTechApi, + customTokensMerger = CustomTokensMerger( + tangemTechApi = tangemTechApi, + userTokensSaver = userTokensSaver, + dispatchers = dispatchers, + ), + userTokensResponseStore = userTokensResponseStore, + userTokensSaver = userTokensSaver, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + expressServiceLoader = expressServiceLoader, + dispatchers = dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index bb4a57959a..595c23de02 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -575,7 +575,7 @@ internal class DefaultCurrenciesRepository( } override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { - return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver + return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY] } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt new file mode 100644 index 0000000000..f9ee73909a --- /dev/null +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt @@ -0,0 +1,481 @@ +package com.tangem.data.tokens + +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.assertEither +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.isMultiCurrency +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] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + + private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val tangemTechApi: TangemTechApi = mockk() + private val customTokensMerger: CustomTokensMerger = mockk() + private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) + private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val expressServiceLoader: ExpressServiceLoader = mockk(relaxUnitFun = true) + + private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher( + demoConfig = DemoConfig(), + userWalletsStore = userWalletsStore, + tangemTechApi = tangemTechApi, + customTokensMerger = customTokensMerger, + userTokensResponseStore = userTokensResponseStore, + userTokensSaver = userTokensSaver, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + expressServiceLoader = expressServiceLoader, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks( + userWalletsStore, + tangemTechApi, + userTokensResponseStore, + userTokensSaver, + cardCryptoCurrencyFactory, + expressServiceLoader, + ) + } + + @Test + fun `fetch failure if UserWallet ISN'T MULTI-CURRENCY wallet`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { isMultiCurrency } returns false + } + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + + // Act + val actual = fetcher(params) + + // Assert + val expected = IllegalStateException("${this::class.simpleName} supports only multi-currency wallet").left() + assertEither(actual, expected) + + verifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) } + coVerify(inverse = true) { + userTokensResponseStore.getSyncOrNull(any()) + } + } + + @Test + fun `fetch successfully if CARD IS DEMO and STORED TOKENS ARE EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "AC01000000041225" + } + + val defaultCoins = listOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), + ) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + userTokensResponseFactory.createResponseToken(defaultCoins.first()), + userTokensResponseFactory.createResponseToken(defaultCoins.last()), + ), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null + every { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + } returns defaultCoins + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + } returns userTokensResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = mockUserWallet.scanResponse) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) + } + } + + @Test + fun `fetch successfully if CARD IS DEMO and STORED TOKENS AREN'T EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "AC01000000041225" + } + + val apiResponse = ApiResponse.Success( + data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + } returns apiResponse.data + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + @Test + fun `fetch successfully if CARD ISN'T DEMO`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + val apiResponse = ApiResponse.Success( + data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + } returns apiResponse.data + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + userTokensResponseStore.getSyncOrNull(userWalletId = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + @Test + fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.TimeoutException, + ) as ApiResponse + + val defaultCoins = listOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), + ) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + userTokensResponseFactory.createResponseToken(defaultCoins.first()), + userTokensResponseFactory.createResponseToken(defaultCoins.last()), + ), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null + coEvery { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + } returns defaultCoins + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + } returns userTokensResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + userTokensSaver.push(userWalletId = any(), response = any()) + } + } + + @Test + fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.TimeoutException, + ) as ApiResponse + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + } returns defaultResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + userTokensSaver.push(userWalletId = any(), response = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + @Test + fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.HttpException( + code = ApiResponseError.HttpException.Code.NOT_FOUND, + message = null, + errorBody = null, + ), + ) as ApiResponse + + val defaultCoins = listOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), + ) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + userTokensResponseFactory.createResponseToken(defaultCoins.first()), + userTokensResponseFactory.createResponseToken(defaultCoins.last()), + ), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null + coEvery { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + } returns defaultCoins + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + } returns userTokensResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) + } + } + + @Test + fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.HttpException( + code = ApiResponseError.HttpException.Code.NOT_FOUND, + message = null, + errorBody = null, + ), + ) as ApiResponse + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + } returns defaultResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + + val defaultResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + UserTokensResponse.Token( + id = null, + networkId = "bitcoin", + derivationPath = null, + name = "Bitcoin", + symbol = "BTC", + decimals = 8, + contractAddress = null, + addresses = listOf(), + ), + ), + ) + + fun UserTokensResponse.toLeastTokens(): List { + return tokens.map { token -> + LeastTokenInfo( + contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = token.networkId, + ) + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt new file mode 100644 index 0000000000..1da20f04b2 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Fetcher of crypto currencies for a multi-currency wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiWalletCryptoCurrenciesFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file