Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-11 15:49:10 +04:00
parent b5f0c9e097
commit 14c007d93a
4 changed files with 190 additions and 34 deletions

View file

@ -14,29 +14,45 @@ tasks.withType<Test>().configureEach {
} }
dependencies { dependencies {
// region Project - Core
implementation(projects.core.datasource) implementation(projects.core.datasource)
implementation(projects.core.utils) api(projects.core.utils)
// endregion
// region Project - Data
implementation(projects.data.common) implementation(projects.data.common)
implementation(projects.data.tokens) // endregion
implementation(projects.domain.appCurrency.models) // region Project - Domain
implementation(projects.domain.models) api(projects.domain.models)
implementation(projects.domain.tokens.models) api(projects.domain.quotes)
implementation(projects.domain.quotes) // endregion
implementation(projects.domain.wallets.models)
// 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.androidx.datastore)
implementation(deps.moshi.kotlin) implementation(deps.moshi.kotlin)
implementation(deps.timber) implementation(deps.timber)
// endregion
implementation(deps.hilt.android) // region Tests
kapt(deps.hilt.kapt)
testImplementation(deps.test.coroutine) testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5) testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine) testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk) testImplementation(deps.test.mockk)
testImplementation(deps.test.truth) testImplementation(deps.test.truth)
testImplementation(projects.common.test) testImplementation(projects.common.test)
// endregion
} }

View file

@ -5,7 +5,7 @@ import com.tangem.data.common.api.safeApiCallWithTimeout
import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.data.quotes.store.setSourceAsCache import com.tangem.data.quotes.store.setSourceAsCache
import com.tangem.data.quotes.store.setSourceAsOnlyCache 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.api.tangemTech.TangemTechApi
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.catchOn
@ -35,8 +35,6 @@ internal class DefaultMultiQuoteFetcher @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : MultiQuoteFetcher { ) : MultiQuoteFetcher {
private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter()
override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) {
if (params.currenciesIds.isEmpty()) { if (params.currenciesIds.isEmpty()) {
Timber.d("No currencies to fetch quotes for") Timber.d("No currencies to fetch quotes for")
@ -45,7 +43,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor(
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( val replacementIdsResult = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies(
currenciesIds = params.currenciesIds.mapTo( currenciesIds = params.currenciesIds.mapTo(
destination = hashSetOf(), destination = hashSetOf(),
transform = CryptoCurrency.RawID::value, transform = CryptoCurrency.RawID::value,
@ -64,7 +62,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor(
onError = { error -> throw error }, onError = { error -> throw error },
) )
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies(
response = response, response = response,
filteredIds = replacementIdsResult.idsFiltered, filteredIds = replacementIdsResult.idsFiltered,
) )

View file

@ -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.blockchain.common.Blockchain
import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds
import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse
/** /** Adapter to replace unsupported currencies for quotes request if it necessary */
* Adapter to replace unsupported currencies for quotes request if it necessary internal object QuotesUnsupportedCurrenciesIdAdapter {
*/
class QuotesUnsupportedCurrenciesIdAdapter {
/** /** Ethereum coin ID */
* Replaces unsupported currencies id to it replacements for request 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<String>): ReplacementResult { fun replaceUnsupportedCurrencies(currenciesIds: Set<String>): ReplacementResult {
val idsForRequest = mutableSetOf<String>() val idsForRequest = mutableSetOf<String>()
val idsFiltered = mutableSetOf<String>() val idsFiltered = mutableSetOf<String>()
currenciesIds.forEach { currencyId -> currenciesIds.forEach { currencyId ->
val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId] val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId]
if (replacementId != null) { if (replacementId != null) {
idsFiltered.add(currencyId) idsFiltered.add(currencyId)
idsForRequest.add(replacementId) idsForRequest.add(replacementId)
@ -25,16 +29,17 @@ class QuotesUnsupportedCurrenciesIdAdapter {
idsForRequest.add(currencyId) idsForRequest.add(currencyId)
} }
} }
return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered) return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered)
} }
/** /** Recover previously replaced currencies [filteredIds] to [response] that uses in application */
* Recover previously replaced currencies to response that uses in application
*/
fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set<String>): QuotesResponse { fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set<String>): QuotesResponse {
val updatedQuotes = mutableMapOf<String, QuotesResponse.Quote>() val updatedQuotes = mutableMapOf<String, QuotesResponse.Quote>()
filteredIds.forEach { filteredId -> filteredIds.forEach { filteredId ->
val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId] val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId]
if (replacementId != null) { if (replacementId != null) {
val quoteReplacement = response.quotes[replacementId] val quoteReplacement = response.quotes[replacementId]
if (quoteReplacement != null) { if (quoteReplacement != null) {
@ -42,16 +47,9 @@ class QuotesUnsupportedCurrenciesIdAdapter {
} }
} }
} }
return response.copy(quotes = updatedQuotes + response.quotes) return response.copy(quotes = updatedQuotes + response.quotes)
} }
data class ReplacementResult(val idsForRequest: Set<String>, val idsFiltered: Set<String>) data class ReplacementResult(val idsForRequest: Set<String>, val idsFiltered: Set<String>)
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 }
}
} }

View file

@ -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<String>,
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<String, Quote>): QuotesResponse {
return QuotesResponse(quotes = quotes.toMap())
}
}
data class GetResponseWithUnsupportedCurrenciesModel(
val response: QuotesResponse,
val filteredIds: Set<String>,
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() }
}
}