Updated on 2026-08-14
This commit is contained in:
parent
b2499c1f35
commit
c8c4f67018
8 changed files with 269 additions and 1 deletions
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.appcurrency
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
|
||||
/**
|
||||
* Store of app currency data model [CurrenciesResponse.Currency]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AppCurrencyResponseStore {
|
||||
|
||||
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.appcurrency
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
|
||||
/**
|
||||
* Default implementation of [AppCurrencyResponseStore]
|
||||
*
|
||||
* @property appPreferencesStore app preferences store
|
||||
*/
|
||||
internal class DefaultAppCurrencyResponseStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : AppCurrencyResponseStore {
|
||||
|
||||
override suspend fun getSyncOrNull(): CurrenciesResponse.Currency? {
|
||||
return appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
|
||||
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.appcurrency.DefaultAppCurrencyResponseStore
|
||||
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -18,4 +21,10 @@ internal object AppCurrencyDataModule {
|
|||
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
|
||||
return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppCurrencyResponseStore(appPreferencesStore: AppPreferencesStore): AppCurrencyResponseStore {
|
||||
return DefaultAppCurrencyResponseStore(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,12 +13,17 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.tokens)
|
||||
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.quotes)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.data.quotes.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.common.api.safeApiCallWithTimeout
|
||||
import com.tangem.data.quotes.store.QuotesStoreV2
|
||||
import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiQuoteFetcher]
|
||||
*
|
||||
* @property tangemTechApi tangemTech api
|
||||
* @property appCurrencyResponseStore app currency response store
|
||||
* @property quotesStore quotes store
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiQuoteFetcher(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
private val quotesStore: QuotesStoreV2,
|
||||
) : MultiQuoteFetcher {
|
||||
|
||||
private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter()
|
||||
|
||||
override suspend fun invoke(params: MultiQuoteFetcher.Params): Either<Throwable, Unit> = Either.catch {
|
||||
quotesStore.refresh(currenciesIds = params.currenciesIds)
|
||||
|
||||
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(
|
||||
currenciesIds = params.currenciesIds.mapTo(
|
||||
destination = hashSetOf(),
|
||||
transform = CryptoCurrency.RawID::value,
|
||||
),
|
||||
)
|
||||
|
||||
val appCurrency = appCurrencyResponseStore.getSyncOrNull()
|
||||
?: error(message = "Unable to get AppCurrency for updating quotes")
|
||||
|
||||
safeApiCallWithTimeout(
|
||||
call = {
|
||||
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
|
||||
val response = tangemTechApi.getQuotes(currencyId = appCurrency.id, coinIds = coinIds).bind()
|
||||
|
||||
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies(
|
||||
response = response,
|
||||
filteredIds = replacementIdsResult.idsFiltered,
|
||||
)
|
||||
|
||||
quotesStore.storeActual(values = updatedResponse.quotes)
|
||||
},
|
||||
onError = { error -> throw error },
|
||||
)
|
||||
}
|
||||
.onLeft {
|
||||
Timber.e(it)
|
||||
quotesStore.storeError(currenciesIds = params.currenciesIds)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.data.quotes.multi
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||
import com.tangem.data.quotes.store.QuotesStoreV2
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiQuoteFetcherTest {
|
||||
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
|
||||
private val quotesStore = mockk<QuotesStoreV2>(relaxed = true)
|
||||
|
||||
private val fetcher = DefaultMultiQuoteFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
quotesStore = quotesStore,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `fetch quotes successfully`() = runTest {
|
||||
val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC,ETH"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = params.currenciesIds)
|
||||
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
|
||||
quotesStore.storeActual(values = successResponse.quotes)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.storeError(currenciesIds = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes failure because api request failed`() = runTest {
|
||||
val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC,ETH"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
|
||||
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = params.currenciesIds)
|
||||
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
|
||||
quotesStore.storeError(currenciesIds = params.currenciesIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.storeActual(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch quotes failure because app currency not found`() = runTest {
|
||||
val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = params.currenciesIds)
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
quotesStore.storeError(currenciesIds = params.currenciesIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
|
||||
quotesStore.storeActual(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val currenciesIds = setOf(
|
||||
CryptoCurrency.RawID(value = "BTC"),
|
||||
CryptoCurrency.RawID(value = "ETH"),
|
||||
)
|
||||
|
||||
val usdAppCurrency = CurrenciesResponse.Currency(
|
||||
id = "USD".lowercase(),
|
||||
code = "USD",
|
||||
name = "US Dollar",
|
||||
unit = "$",
|
||||
type = "fiat",
|
||||
rateBTC = "",
|
||||
)
|
||||
|
||||
val successResponse = QuotesResponse(
|
||||
quotes = mapOf(
|
||||
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
|
||||
"ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
|||
/**
|
||||
* Adapter to replace unsupported currencies for quotes request if it necessary
|
||||
*/
|
||||
internal class QuotesUnsupportedCurrenciesIdAdapter {
|
||||
class QuotesUnsupportedCurrenciesIdAdapter {
|
||||
|
||||
/**
|
||||
* Replaces unsupported currencies id to it replacements for request
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.quotes.multi
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
/**
|
||||
* Fetcher of quotes
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MultiQuoteFetcher : FlowFetcher<MultiQuoteFetcher.Params> {
|
||||
|
||||
data class Params(val currenciesIds: Set<CryptoCurrency.RawID>)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue