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
|
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.AvailableAppCurrenciesStore
|
||||||
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
|
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
|
||||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||||
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -18,4 +21,10 @@ internal object AppCurrencyDataModule {
|
||||||
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
|
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
|
||||||
return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore())
|
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.datasource)
|
||||||
implementation(projects.core.utils)
|
implementation(projects.core.utils)
|
||||||
|
|
||||||
|
implementation(projects.data.common)
|
||||||
|
implementation(projects.data.tokens)
|
||||||
|
|
||||||
|
implementation(projects.domain.appCurrency.models)
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
implementation(projects.domain.tokens.models)
|
implementation(projects.domain.tokens.models)
|
||||||
implementation(projects.domain.quotes)
|
implementation(projects.domain.quotes)
|
||||||
implementation(projects.domain.wallets.models)
|
implementation(projects.domain.wallets.models)
|
||||||
|
|
||||||
implementation(deps.androidx.datastore)
|
implementation(deps.androidx.datastore)
|
||||||
|
implementation(deps.timber)
|
||||||
|
|
||||||
implementation(deps.hilt.android)
|
implementation(deps.hilt.android)
|
||||||
kapt(deps.hilt.kapt)
|
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
|
* 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
|
* 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