From 14c007d93a1cb90a1c216e41d9ec1c4d25d7b831 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 15:49:10 +0400 Subject: [PATCH] Updated on 2026-08-14 --- data/quotes/build.gradle.kts | 36 +++-- .../quotes/multi/DefaultMultiQuoteFetcher.kt | 8 +- .../QuotesUnsupportedCurrenciesIdAdapter.kt | 36 +++-- ...uotesUnsupportedCurrenciesIdAdapterTest.kt | 144 ++++++++++++++++++ 4 files changed, 190 insertions(+), 34 deletions(-) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens => quotes/src/main/java/com/tangem/data/quotes}/utils/QuotesUnsupportedCurrenciesIdAdapter.kt (70%) create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 41c386a5c4..2549fee133 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -14,29 +14,45 @@ tasks.withType().configureEach { } dependencies { + // region Project - Core implementation(projects.core.datasource) - implementation(projects.core.utils) + api(projects.core.utils) + // endregion + // region Project - Data implementation(projects.data.common) - implementation(projects.data.tokens) + // endregion - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.models) - implementation(projects.domain.tokens.models) - implementation(projects.domain.quotes) - implementation(projects.domain.wallets.models) + // region Project - Domain + api(projects.domain.models) + api(projects.domain.quotes) + // endregion + // region Project - Libs + implementation(projects.libs.blockchainSdk) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Tangem SDKs + implementation(tangemDeps.blockchain) + // endregion + + // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) implementation(deps.timber) + // endregion - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - + // region Tests testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) + // endregion } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt index 830d3afef4..1f09c109a0 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -5,7 +5,7 @@ import com.tangem.data.common.api.safeApiCallWithTimeout import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.data.quotes.store.setSourceAsCache import com.tangem.data.quotes.store.setSourceAsOnlyCache -import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter +import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.catchOn @@ -35,8 +35,6 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteFetcher { - private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() - override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { if (params.currenciesIds.isEmpty()) { Timber.d("No currencies to fetch quotes for") @@ -45,7 +43,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) - val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( + val replacementIdsResult = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( currenciesIds = params.currenciesIds.mapTo( destination = hashSetOf(), transform = CryptoCurrency.RawID::value, @@ -64,7 +62,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( onError = { error -> throw error }, ) - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( response = response, filteredIds = replacementIdsResult.idsFiltered, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt similarity index 70% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index bf7779378a..72c9a1f66c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -1,23 +1,27 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.quotes.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.QuotesResponse -/** - * Adapter to replace unsupported currencies for quotes request if it necessary - */ -class QuotesUnsupportedCurrenciesIdAdapter { +/** Adapter to replace unsupported currencies for quotes request if it necessary */ +internal object QuotesUnsupportedCurrenciesIdAdapter { - /** - * Replaces unsupported currencies id to it replacements for request - */ + /** Ethereum coin ID */ + private val ethCoinId = Blockchain.Ethereum.toCoinId() + + /** Map that contains unsupported currencies and their replacement for request */ + private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } + + /** Replaces unsupported currencies id [currenciesIds] to it replacements for request */ fun replaceUnsupportedCurrencies(currenciesIds: Set): ReplacementResult { val idsForRequest = mutableSetOf() val idsFiltered = mutableSetOf() + currenciesIds.forEach { currencyId -> val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId] + if (replacementId != null) { idsFiltered.add(currencyId) idsForRequest.add(replacementId) @@ -25,16 +29,17 @@ class QuotesUnsupportedCurrenciesIdAdapter { idsForRequest.add(currencyId) } } + return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered) } - /** - * Recover previously replaced currencies to response that uses in application - */ + /** Recover previously replaced currencies [filteredIds] to [response] that uses in application */ fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set): QuotesResponse { val updatedQuotes = mutableMapOf() + filteredIds.forEach { filteredId -> val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId] + if (replacementId != null) { val quoteReplacement = response.quotes[replacementId] if (quoteReplacement != null) { @@ -42,16 +47,9 @@ class QuotesUnsupportedCurrenciesIdAdapter { } } } + return response.copy(quotes = updatedQuotes + response.quotes) } data class ReplacementResult(val idsForRequest: Set, val idsFiltered: Set) - - companion object { - /** - * Map that contains unsupported currencies and their replacement for request - */ - private val ethCoinId = Blockchain.Ethereum.toCoinId() - private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } - } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt new file mode 100644 index 0000000000..3a4f3abb14 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt @@ -0,0 +1,144 @@ +package com.tangem.data.quotes.utils + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter.ReplacementResult +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.api.tangemTech.models.QuotesResponse.Quote +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesUnsupportedCurrenciesIdAdapterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceUnsupportedCurrencies { + + @ParameterizedTest + @ProvideTestModels + fun replaceUnsupportedCurrencies(model: ReplaceUnsupportedCurrenciesModel) { + // Act + val actual = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( + currenciesIds = model.currenciesIds, + ) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + ReplaceUnsupportedCurrenciesModel( + currenciesIds = l2BlockchainsCoinIds.toSet(), + expected = ReplacementResult( + idsForRequest = l2BlockchainsCoinIds.mapTo(hashSetOf()) { + Blockchain.Ethereum.toCoinId() + }, + idsFiltered = l2BlockchainsCoinIds.toSet(), + ), + ), + ReplaceUnsupportedCurrenciesModel( + currenciesIds = notL2BlockchainsCoinIds, + expected = ReplacementResult(idsForRequest = notL2BlockchainsCoinIds, idsFiltered = emptySet()), + ), + ReplaceUnsupportedCurrenciesModel( + currenciesIds = blockchainCoinIds, + expected = ReplacementResult( + idsForRequest = Blockchain.entries + .filterNot(Blockchain::isTestnet) + .mapTo(hashSetOf()) { + if (it.isL2EthereumNetwork()) { + Blockchain.Ethereum.toCoinId() + } else { + it.toCoinId() + } + }, + idsFiltered = l2BlockchainsCoinIds.toSet(), + ), + ), + ) + } + + data class ReplaceUnsupportedCurrenciesModel( + val currenciesIds: Set, + val expected: ReplacementResult, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetResponseWithUnsupportedCurrencies { + + private val zeroQuote = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ZERO) + private val ethQuote = "ethereum" to zeroQuote + + @ParameterizedTest + @ProvideTestModels + fun getResponseWithUnsupportedCurrencies(model: GetResponseWithUnsupportedCurrenciesModel) { + // Act + val actual = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( + response = model.response, + filteredIds = model.filteredIds, + ) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetResponseWithUnsupportedCurrenciesModel( + response = QuotesResponse(quotes = emptyMap()), + filteredIds = emptySet(), + expected = QuotesResponse(quotes = emptyMap()), + ), + GetResponseWithUnsupportedCurrenciesModel( + response = createQuotesResponse(ethQuote), + filteredIds = emptySet(), + expected = createQuotesResponse(ethQuote), + ), + GetResponseWithUnsupportedCurrenciesModel( + response = createQuotesResponse( + "arbitrum-one" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ethQuote, + ), + filteredIds = blockchainCoinIds, + expected = createQuotesResponse( + *l2BlockchainsCoinIds.map { it to zeroQuote }.toTypedArray(), + "arbitrum-one" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ethQuote, + ), + ), + ) + + private fun createQuotesResponse(vararg quotes: Pair): QuotesResponse { + return QuotesResponse(quotes = quotes.toMap()) + } + } + + data class GetResponseWithUnsupportedCurrenciesModel( + val response: QuotesResponse, + val filteredIds: Set, + val expected: QuotesResponse, + ) + + private companion object { + + val blockchainCoinIds = Blockchain.entries + .filterNot(Blockchain::isTestnet) + .mapTo(destination = hashSetOf(), transform = Blockchain::toCoinId) + + val notL2BlockchainsCoinIds = Blockchain.entries + .filterNot { it.isTestnet() || it.isL2EthereumNetwork() } + .mapTo(hashSetOf()) { it.toCoinId() } + } +} \ No newline at end of file