Updated on 2026-08-14
This commit is contained in:
parent
73344b92dc
commit
06cd34a35a
8 changed files with 243 additions and 8 deletions
|
|
@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
|
|||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection
|
||||
import com.tangem.datasource.local.nft.custom.NFTPriceId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultNFTPersistenceStore(
|
||||
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
|
||||
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
|
||||
private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
|
||||
) : NFTPersistenceStore {
|
||||
|
||||
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
|
||||
|
|
@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore(
|
|||
}
|
||||
|
||||
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTAsset.SalePrice?> = pricesPersistenceStore.data
|
||||
.map { it[assetId] }
|
||||
.map { data -> data.associate { it.assetId to it.price }[assetId] }
|
||||
|
||||
override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
|
||||
.data
|
||||
.firstOrNull()
|
||||
?.associate { it.assetId to it.price }
|
||||
|
||||
override suspend fun saveCollections(collections: List<NFTCollection>) {
|
||||
collectionsPersistenceStore.updateData {
|
||||
|
|
@ -41,7 +43,7 @@ internal class DefaultNFTPersistenceStore(
|
|||
|
||||
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
|
||||
pricesPersistenceStore.updateData {
|
||||
it.toMutableMap().apply { this[assetId] = salePrice }
|
||||
it.toMutableList().plus(NFTPriceId(assetId = assetId, price = salePrice))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.nft.custom.NFTPriceId
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithCustomKeyTypes
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor(
|
|||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
|
||||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
|
||||
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
|
||||
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
|
||||
defaultValue = emptyMap(),
|
||||
types = listTypes<NFTPriceId>(),
|
||||
defaultValue = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.datasource.local.nft.custom
|
||||
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
|
||||
data class NFTPriceId(
|
||||
val assetId: NFTAsset.Identifier,
|
||||
val price: NFTAsset.SalePrice,
|
||||
)
|
||||
|
|
@ -2,8 +2,10 @@ package com.tangem.data.quotes.di
|
|||
|
||||
import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher
|
||||
import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater
|
||||
import com.tangem.data.quotes.single.DefaultSingleQuoteFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
|
||||
import com.tangem.domain.quotes.single.SingleQuoteFetcher
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -21,4 +23,8 @@ internal interface QuoteFetcherModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindMultiQuoteUpdater(impl: DefaultMultiQuoteUpdater): MultiQuoteUpdater
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleQuoteFetcher(impl: DefaultSingleQuoteFetcher): SingleQuoteFetcher
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.data.quotes.single
|
||||
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteFetcher
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSingleQuoteFetcher @Inject constructor(
|
||||
private val multiQuoteFetcher: MultiQuoteFetcher,
|
||||
) : SingleQuoteFetcher {
|
||||
|
||||
override suspend fun invoke(params: SingleQuoteFetcher.Params) = multiQuoteFetcher.invoke(
|
||||
MultiQuoteFetcher.Params(
|
||||
currenciesIds = setOf(params.rawCurrencyId),
|
||||
appCurrencyId = params.appCurrencyId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package com.tangem.data.quotes.single
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||
import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher
|
||||
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.single.SingleQuoteFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
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
|
||||
|
||||
internal class DefaultSingleQuoteFetcherTest {
|
||||
|
||||
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
|
||||
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
|
||||
private val quotesStore = mockk<QuotesStoreV2>(relaxed = true)
|
||||
|
||||
private val multiFetcher = DefaultMultiQuoteFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
appCurrencyResponseStore = appCurrencyResponseStore,
|
||||
quotesStore = quotesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val singleFetcher = DefaultSingleQuoteFetcher(multiFetcher)
|
||||
|
||||
@Test
|
||||
fun `fetch single quote successfully`() = runTest {
|
||||
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId))
|
||||
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 single quote successfully if appCurrencyId from params is not null`() = runTest {
|
||||
val appCurrencyId = "usd"
|
||||
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
|
||||
|
||||
val coinIds = "BTC"
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
|
||||
} returns ApiResponse.Success(successResponse)
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = setOf(currenciesId))
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
quotesStore.storeActual(values = successResponse.quotes)
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest {
|
||||
val appCurrencyId = ""
|
||||
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = setOf(currenciesId))
|
||||
quotesStore.storeError(currenciesIds = setOf(currenciesId))
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
|
||||
.isEqualTo("Unable to get AppCurrency for updating quotes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote failure because api request failed`() = runTest {
|
||||
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val coinIds = "BTC"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
|
||||
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = setOf(currenciesId))
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
|
||||
quotesStore.storeError(currenciesIds = setOf(currenciesId))
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.storeActual(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch single quote failure because app currency not found`() = runTest {
|
||||
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
|
||||
|
||||
val actual = singleFetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.refresh(currenciesIds = setOf(currenciesId))
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
quotesStore.storeError(currenciesIds = setOf(currenciesId))
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
|
||||
quotesStore.storeActual(values = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val currenciesId = CryptoCurrency.RawID(value = "BTC")
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -190,12 +190,19 @@ internal class DefaultTransactionRepository(
|
|||
null
|
||||
}
|
||||
|
||||
val contractAddress = when (val identifier = nftAsset.identifier) {
|
||||
is NFTAsset.Identifier.EVM -> identifier.tokenAddress
|
||||
is NFTAsset.Identifier.Solana -> identifier.tokenAddress
|
||||
is NFTAsset.Identifier.TON -> identifier.tokenAddress
|
||||
NFTAsset.Identifier.Unknown -> ""
|
||||
}
|
||||
|
||||
return@withContext createTransaction(
|
||||
amount = Amount(
|
||||
value = nftAsset.amount?.toBigDecimal() ?: error("Invalid amount"),
|
||||
token = Token(
|
||||
symbol = blockchain.currency,
|
||||
contractAddress = "",
|
||||
contractAddress = contractAddress,
|
||||
decimals = nftAsset.decimals ?: error("Invalid decimals"),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.quotes.single
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
/**
|
||||
* Fetcher of quote for [CryptoCurrency.RawID]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SingleQuoteFetcher : FlowFetcher<SingleQuoteFetcher.Params> {
|
||||
|
||||
/**
|
||||
* Params
|
||||
*
|
||||
* @property rawCurrencyId crypto currency id
|
||||
*/
|
||||
data class Params(
|
||||
val rawCurrencyId: CryptoCurrency.RawID,
|
||||
val appCurrencyId: String?,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue