diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 090a030029..1a79b6209b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -83,4 +85,22 @@ internal object NFTDomainModule { GetNFTExploreUrlUseCase( nftRepository = nftRepository, ) + + @Provides + @Singleton + fun provideGetNFTPriceUseCase( + nftRepository: NFTRepository, + singleQuoteSupplier: SingleQuoteSupplier, + ): GetNFTPriceUseCase { + return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier) + } + + @Provides + @Singleton + fun provideFetchNFTPriceUseCase( + nftRepository: NFTRepository, + singleQuoteFetcher: SingleQuoteFetcher, + ): FetchNFTPriceUseCase { + return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index 42d11c055f..51b17b2bbf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -43,7 +43,7 @@ internal class DefaultNFTPersistenceStore( override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) { pricesPersistenceStore.updateData { - it.toMutableList().plus(NFTPriceId(assetId = assetId, price = salePrice)) + it.toMutableList() + NFTPriceId(assetId = assetId, price = salePrice) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt index 252753005a..f0741130bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt @@ -5,14 +5,16 @@ import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.utils.converter.TwoWayConverter import com.tangem.blockchain.nft.models.NFTAsset.SalePrice as SDKSalePrice -internal class NFTSdkAssetSalePriceConverter( +class NFTSdkAssetSalePriceConverter( private val assetId: NFTAsset.Identifier, ) : TwoWayConverter { override fun convert(value: SDKSalePrice): NFTSalePrice.Value { return NFTSalePrice.Value( assetId = assetId, + fiatValue = null, value = value.value, - symbol = value.symbol, + symbol = value.symbol.orEmpty(), + decimals = value.decimals ?: 0, ) } @@ -20,6 +22,7 @@ internal class NFTSdkAssetSalePriceConverter( return SDKSalePrice( symbol = value.symbol, value = value.value, + decimals = value.decimals, ) } } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 28b4ba6492..2747f7c697 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -4,12 +4,14 @@ import arrow.core.Either import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.nft.NFTPersistenceStore import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory import com.tangem.datasource.local.nft.NFTRuntimeStore import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter +import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -19,6 +21,7 @@ import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -30,7 +33,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import java.lang.UnsupportedOperationException +import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -49,13 +52,62 @@ internal class DefaultNFTRepository @Inject constructor( private val networkJobs = ConcurrentHashMap() private val collectionJobs = ConcurrentHashMap() + private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) private val nftRuntimeStores = ConcurrentHashMap() private val nftPersistenceStores = ConcurrentHashMap() + private val collectionIdConverter = NFTSdkCollectionIdentifierConverter + private val assetIdConverter = NFTSdkAssetIdentifierConverter + override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } + override fun getNFTCurrency(network: Network): CryptoCurrency { + return cryptoCurrencyFactory.createCoin(network) + } + + override suspend fun getNFTSalePrice( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + assetId: NFTAsset.Identifier, + ): NFTSalePrice = withContext(dispatchers.io) { + val salePriceConverter = NFTSdkAssetSalePriceConverter(assetId) + + runCatching { + saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) + + val sdkPrice = walletManagersFacade.getNFTSalePrice( + userWalletId = userWalletId, + network = network, + collectionIdentifier = collectionIdConverter.convertBack(collectionId), + assetIdentifier = assetIdConverter.convertBack(assetId), + ) + val nftCurrency = getNFTCurrency(network) + val salePrice = sdkPrice?.let { + val convertedPrice = salePriceConverter.convert(sdkPrice) + convertedPrice.copy( + value = convertedPrice.value.movePointLeft(nftCurrency.decimals), + decimals = nftCurrency.decimals, + symbol = nftCurrency.symbol, + ) + } ?: NFTSalePrice.Empty(assetId) + + saveSalePriceInRuntime(userWalletId, network, salePrice) + + sdkPrice?.let { + val sdkAssetId = assetIdConverter.convertBack(assetId) + saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) + } + + salePrice + }.getOrElse { + saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Error(assetId)) + NFTSalePrice.Error(assetId) + } + } + private suspend fun observeCollectionsInternal( userWalletId: UserWalletId, networks: List, @@ -86,7 +138,7 @@ internal class DefaultNFTRepository @Inject constructor( ) = coroutineScope { launch(dispatchers.io) { Either.catch { - val sdkCollectionId = NFTSdkCollectionIdentifierConverter.convertBack(collectionId) + val sdkCollectionId = collectionIdConverter.convertBack(collectionId) val assets = walletManagersFacade.getNFTAssets( userWalletId = userWalletId, @@ -97,7 +149,7 @@ internal class DefaultNFTRepository @Inject constructor( expireAssets(userWalletId, network, collectionId) assets.forEach { - val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier) + val assetId = assetIdConverter.convert(it.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) @@ -157,7 +209,7 @@ internal class DefaultNFTRepository @Inject constructor( override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? = walletManagersFacade.getNFTExploreUrl( network = network, - assetIdentifier = NFTSdkAssetIdentifierConverter.convertBack(assetIdentifier), + assetIdentifier = assetIdConverter.convertBack(assetIdentifier), ) private suspend fun refreshCollectionsInternal( @@ -190,7 +242,7 @@ internal class DefaultNFTRepository @Inject constructor( refreshAssets( userWalletId = userWalletId, network = network, - collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier), + collectionId = collectionIdConverter.convert(collection.identifier), ) } } @@ -215,29 +267,16 @@ internal class DefaultNFTRepository @Inject constructor( sdkAssetId: SdkNFTAsset.Identifier, ) = coroutineScope { launch(dispatchers.io) { - val assetId = NFTSdkAssetIdentifierConverter.convert(sdkAssetId) + val assetId = assetIdConverter.convert(sdkAssetId) + val collectionId = collectionIdConverter.convert(sdkCollectionId) Either.catch { - saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) - - val sdkSalePrice = - walletManagersFacade.getNFTSalePrice(userWalletId, network, sdkCollectionId, sdkAssetId) - - val salePrice = if (sdkSalePrice == null) { - NFTSalePrice.Empty(assetId) - } else { - NFTSalePrice.Value( - assetId = assetId, - value = sdkSalePrice.value, - symbol = sdkSalePrice.symbol, - ) - } - - saveSalePriceInRuntime(userWalletId, network, salePrice) - - sdkSalePrice?.let { - saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) - } + getNFTSalePrice( + userWalletId = userWalletId, + network = network, + collectionId = collectionId, + assetId = assetId, + ) }.onLeft { saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Error(assetId)) } @@ -396,14 +435,17 @@ internal class DefaultNFTRepository @Inject constructor( prices .mapKeys { val (assetId, _) = it - NFTSdkAssetIdentifierConverter.convert(assetId) + assetIdConverter.convert(assetId) } .mapValues { val (assetId, price) = it + val nftCurrency = getNFTCurrency(network) NFTSalePrice.Value( assetId = assetId, - value = price.value, - symbol = price.symbol, + value = price.value.movePointLeft(nftCurrency.decimals), + fiatValue = null, + symbol = nftCurrency.symbol, + decimals = nftCurrency.decimals, ) } } diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index 518f509278..28e807c797 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -22,4 +22,5 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.quotes) } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt index 52cc43d072..43bc8a4a29 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt @@ -26,6 +26,8 @@ sealed class NFTSalePrice { data class Value( override val assetId: NFTAsset.Identifier, val value: SerializedBigDecimal, + val fiatValue: SerializedBigDecimal?, val symbol: String, + val decimals: Int, ) : NFTSalePrice() } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt new file mode 100644 index 0000000000..fafac1aea8 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.nft + +import arrow.core.Either +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.tokens.model.Network + +class FetchNFTPriceUseCase( + private val nftRepository: NFTRepository, + private val singleQuoteFetcher: SingleQuoteFetcher, +) { + + suspend operator fun invoke(network: Network, appCurrencyId: String?): Either { + return Either.catch { + val nftCurrency = nftRepository.getNFTCurrency(network) + val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") + + singleQuoteFetcher( + params = SingleQuoteFetcher.Params( + rawCurrencyId = rawId, + appCurrencyId = appCurrencyId, + ), + ) + } + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt new file mode 100644 index 0000000000..4dc44a7590 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.nft + +import arrow.core.Either +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNFTPriceUseCase( + private val nftRepository: NFTRepository, + private val singleQuoteSupplier: SingleQuoteSupplier, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, nftAsset: NFTAsset): Either> { + return Either.catch { + val nftCurrency = nftRepository.getNFTCurrency(nftAsset.network) + val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") + + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + ).map { quote -> + val nftPrice = nftRepository.getNFTSalePrice( + userWalletId = userWalletId, + network = nftAsset.network, + collectionId = nftAsset.collectionId, + assetId = nftAsset.id, + ) + + val quoteValue = quote as? Quote.Value + + if (nftPrice !is NFTSalePrice.Value) { + nftPrice + } else { + nftPrice.copy(fiatValue = quoteValue?.fiatRate?.multiply(nftPrice.value)) + } + } + } + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 1484b241a3..5f8817884c 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -3,6 +3,8 @@ package com.tangem.domain.nft.repository import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -10,6 +12,15 @@ import kotlinx.coroutines.flow.Flow interface NFTRepository { fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> + fun getNFTCurrency(network: Network): CryptoCurrency + + suspend fun getNFTSalePrice( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + assetId: NFTAsset.Identifier, + ): NFTSalePrice + suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier)