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/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index c39150c2c2..be40dc6f87 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -13,4 +13,10 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle override fun getCardId(): String { return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: "" } + + override fun getCardsPublicKeys(): Map { + return userWalletsListManager.userWalletsSync.associate { + it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index a11b7bf574..7945fc59eb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -11,4 +11,9 @@ interface AuthProvider { fun getCardPublicKey(): String fun getCardId(): String + + /** + * Returns map where keys(cardId) associated with cardPublicKey + */ + fun getCardsPublicKeys(): Map } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt new file mode 100644 index 0000000000..80454a733b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CardInfoBody( + @Json(name = "card_id") val cardId: String, + @Json(name = "card_public_key") val cardPublicKey: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt index dbadddd6fc..3fd5ff6196 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt @@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WalletIdBody( @Json(name = "id") val walletId: String, + @Json(name = "name") val name: String, + @Json(name = "cards") val cards: List, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt new file mode 100644 index 0000000000..1165379d12 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di.local + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.DefaultUserTokensResponseStore +import com.tangem.datasource.local.token.UserTokensResponseStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object LocalTokenModule { + + @Provides + @Singleton + fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore { + return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore) + } +} \ 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 a418b31a5d..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 @@ -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>, - private val pricesPersistenceStore: DataStore>, + private val pricesPersistenceStore: DataStore>, ) : NFTPersistenceStore { override fun getCollections(): Flow?> = collectionsPersistenceStore.data @@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore( } override fun getSalePrice(assetId: NFTAsset.Identifier): Flow = pricesPersistenceStore.data - .map { it[assetId] } + .map { data -> data.associate { it.assetId to it.price }[assetId] } override suspend fun getSalePricesSync(): Map? = pricesPersistenceStore .data .firstOrNull() + ?.associate { it.assetId to it.price } override suspend fun saveCollections(collections: List) { 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() + NFTPriceId(assetId = assetId, price = salePrice) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt index cb2e6399bf..2dad3c41c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt @@ -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(), - defaultValue = emptyMap(), + types = listTypes(), + defaultValue = emptyList(), ), ) } 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/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt new file mode 100644 index 0000000000..0f933cd743 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt new file mode 100644 index 0000000000..99d24825e5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.local.nft.custom + +import com.tangem.blockchain.nft.models.NFTAsset + +data class NFTPriceKeyValue( + val key: NFTAsset.Identifier, + val value: NFTAsset.SalePrice, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt new file mode 100644 index 0000000000..c04690b7ce --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Default implementation of [UserTokensResponseStore] + * + * @property appPreferencesStore app preferences store + * +[REDACTED_AUTHOR] + */ +internal class DefaultUserTokensResponseStore( + private val appPreferencesStore: AppPreferencesStore, +) : UserTokensResponseStore { + + override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt new file mode 100644 index 0000000000..df802c591c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Store of [UserTokensResponse] + * +[REDACTED_AUTHOR] + */ +interface UserTokensResponseStore { + + /** Get [UserTokensResponse] synchronously by [userWalletId] or null */ + suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? +} \ No newline at end of file diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index 96dc4861ca..05ffe54f76 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -14,9 +14,11 @@ dependencies { implementation(projects.core.datasource) /* Domain */ - implementation(projects.domain.models) + implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /* Libs - SDK */ implementation(tangemDeps.blockchain) @@ -28,8 +30,16 @@ dependencies { kapt(deps.hilt.kapt) /* Libs - Other */ - implementation(deps.kotlin.coroutines) - implementation(deps.jodatime) - implementation(deps.timber) + implementation(deps.androidx.datastore) implementation(deps.arrow.core) + implementation(deps.jodatime) + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + + /* Test */ + testImplementation(projects.common.test) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt new file mode 100644 index 0000000000..fca4ffba1a --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt @@ -0,0 +1,46 @@ +package com.tangem.data.common.currency + +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Factory for creating list of [CryptoCurrency] for selected card + * +[REDACTED_AUTHOR] + */ +interface CardCryptoCurrencyFactory { + + /** + * Universal method for creating list of [CryptoCurrency] in [network] for any card + * + * @param userWalletId user wallet id that determines type of card + * @param network network + */ + @Throws + suspend fun create(userWalletId: UserWalletId, network: Network): List + + /** + * Create default coins for multi currency card + * + * @param scanResponse scan response + */ + fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List + + /** + * Create primary currency for single currency card + * + * @param scanResponse scan response + */ + @Throws + fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency + + /** + * Create currencies for single currency card with token (like, NODL) + * + * @param scanResponse scan response + */ + @Throws + fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt new file mode 100644 index 0000000000..c4da63c87c --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -0,0 +1,128 @@ +package com.tangem.data.common.currency + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Default implementation of factory for creating list of [CryptoCurrency] for selected card + * + * @property demoConfig demo config + * @property excludedBlockchains excluded blockchains + * @property userWalletsStore user wallets store + * @property userTokensResponseStore user tokens response store + */ +internal class DefaultCardCryptoCurrencyFactory( + private val demoConfig: DemoConfig, + private val excludedBlockchains: ExcludedBlockchains, + private val userWalletsStore: UserWalletsStore, + private val userTokensResponseStore: UserTokensResponseStore, +) : CardCryptoCurrencyFactory { + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } + + override suspend fun create(userWalletId: UserWalletId, network: Network): List { + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + + val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + + // multi-currency wallet + if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) + + // check if the blockchain of single-currency wallet is the same as network + val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + if (cardBlockchain != blockchain) return emptyList() + + // single-currency wallet with token (NODL) + if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } + + // single-currency wallet + return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) + } + + override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { + val card = scanResponse.card + + var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + + if (card.isTestCard) { + blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + } + + return blockchains.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + extraDerivationPath = null, + scanResponse = scanResponse, + ) + } + } + + override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { + return with(getSingleWalletCurrencies(scanResponse)) { + primaryToken ?: coin + } + } + + override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { + return with(getSingleWalletCurrencies(scanResponse)) { + listOfNotNull(coin, primaryToken) + } + } + + private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { + val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) + ?: return emptyList() + + val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + + return responseCurrenciesFactory.createCurrencies( + tokens = response.tokens.filter { + it.networkId == network.backendId && it.derivationPath == network.derivationPath.value + }, + scanResponse = userWallet.scanResponse, + ) + } + + private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies { + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + scanResponse = scanResponse, + ) + + requireNotNull(coin) { "Coin for the single currency card cannot be null" } + + val primaryToken = resolver.getPrimaryToken()?.let { token -> + cryptoCurrencyFactory.createToken( + sdkToken = token, + blockchain = blockchain, + extraDerivationPath = null, + scanResponse = scanResponse, + ) + } + + return SingleWalletCurrencies(coin = coin, primaryToken = primaryToken) + } + + private data class SingleWalletCurrencies(val coin: CryptoCurrency, val primaryToken: CryptoCurrency?) +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt new file mode 100644 index 0000000000..c9f546e6dd --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -0,0 +1,33 @@ +package com.tangem.data.common.di + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DataCommonModule { + + @Provides + @Singleton + fun provideCardCryptoCurrencyFactory( + excludedBlockchains: ExcludedBlockchains, + userWalletsStore: UserWalletsStore, + userTokensResponseStore: UserTokensResponseStore, + ): CardCryptoCurrencyFactory { + return DefaultCardCryptoCurrencyFactory( + demoConfig = DemoConfig(), + excludedBlockchains = excludedBlockchains, + userWalletsStore = userWalletsStore, + userTokensResponseStore = userTokensResponseStore, + ) + } +} \ No newline at end of file diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt new file mode 100644 index 0000000000..e6b868dc39 --- /dev/null +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -0,0 +1,430 @@ +package com.tangem.data.common.currency + +import android.net.Uri +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.card.WalletData +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.GenericCardConfig +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultCardCryptoCurrencyFactoryTest { + + private val userWalletsStore: UserWalletsStore = mockk() + private val userTokensResponseStore: UserTokensResponseStore = mockk() + + private val factory = DefaultCardCryptoCurrencyFactory( + demoConfig = DemoConfig(), + excludedBlockchains = ExcludedBlockchains(), + userWalletsStore = userWalletsStore, + userTokensResponseStore = userTokensResponseStore, + ) + + @Before + fun setup() { + mockkStatic(Uri::class) + every { Uri.parse(any()) } returns mockk() + } + + @Test + fun `test create if userTokensResponse is not empty`() = runTest { + val multiWallet = createMultiWallet() + + val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = listOf(ethereum), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = listOf(ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if userTokensResponse is empty`() = runTest { + val multiWallet = createMultiWallet() + + val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = listOf(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if userTokensResponse is null`() = runTest { + val multiWallet = createMultiWallet() + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns null + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if userTokensResponse does not contain currency of selected network`() = runTest { + val multiWallet = createMultiWallet() + + val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = listOf(bitcoin), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if single wallet has another primary network`() = runTest { + val singleWallet = createSingleWallet() + + coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet + + val actual = factory.create(userWalletId = singleWallet.walletId, network = bitcoin.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = singleWallet.walletId) + singleWallet.scanResponse.cardTypesResolver.getBlockchain() + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if card is single wallet`() = runTest { + val singleWallet = createSingleWallet() + + coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet + + val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = singleWallet.walletId) + singleWallet.scanResponse.cardTypesResolver.getBlockchain() + } + + val expected = listOf(ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if card is single wallet with token`() = runTest { + val singleWallet = createSingleWalletWithToken() + + coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet + + val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network) + + val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = Blockchain.Ethereum, + extraDerivationPath = null, + scanResponse = singleWallet.scanResponse, + ) + val expected = listOf(ethereum, token) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createDefaultCoinsForMultiCurrencyCard if card is prod`() = runTest { + val multiWallet = createMultiWallet() + + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + val expected = listOf(bitcoin, ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createDefaultCoinsForMultiCurrencyCard if card is test`() = runTest { + val multiWallet = createMultiWallet().let { + it.copy( + scanResponse = it.scanResponse.copy( + card = it.scanResponse.card.copy(cardId = "FF99", batchId = "99FF"), + ), + ) + } + + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + val expected = listOf( + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet), + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createDefaultCoinsForMultiCurrencyCard if card is demo`() = runTest { + val multiWallet = createMultiWallet().let { + it.copy( + scanResponse = it.scanResponse.copy( + card = it.scanResponse.card.copy(cardId = "AC01000000041225"), + ), + ) + } + + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + val expected = listOf( + bitcoin, + ethereum, + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin), + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createPrimaryCurrencyForSingleCurrencyCard if unable to create token`() = runTest { + val singleWallet = UserWallet( + name = "Note", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ), + hasBackupError = false, + ) + + val actual = runCatching { + factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + } + + val exception = IllegalArgumentException("Coin for the single currency card cannot be null") + + Truth.assertThat(actual.isFailure).isTrue() + Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) + } + + @Test + fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is null`() = runTest { + val singleWallet = createSingleWallet() + + val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + + val expected = ethereum + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is not null`() = runTest { + val singleWallet = createSingleWalletWithToken() + + val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + + val expected = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = Blockchain.Ethereum, + extraDerivationPath = null, + scanResponse = singleWallet.scanResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createCurrenciesForSingleCurrencyCardWithToken if unable to create token`() = runTest { + val singleWalletWithToken = UserWallet( + name = "Note", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ), + hasBackupError = false, + ) + + val actual = runCatching { + factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = singleWalletWithToken.scanResponse) + } + + val exception = IllegalArgumentException("Coin for the single currency card cannot be null") + + Truth.assertThat(actual.isFailure).isTrue() + Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) + } + + @Test + fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is null`() = runTest { + val singleWalletWithToken = createSingleWallet() + + val actual = factory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = singleWalletWithToken.scanResponse, + ) + + val expected = listOf(ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is not null`() = runTest { + val singleWalletWithToken = createSingleWalletWithToken() + + val actual = factory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = singleWalletWithToken.scanResponse, + ) + + val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = singleWalletWithToken.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = Blockchain.Ethereum, + extraDerivationPath = null, + scanResponse = singleWalletWithToken.scanResponse, + ) + + val expected = listOf(ethereum, token) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun createMultiWallet(): UserWallet { + return UserWallet( + name = "Wallet 1", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = true, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ), + hasBackupError = false, + ) + } + + private fun createSingleWallet(): UserWallet { + return UserWallet( + name = "Note", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ).let { + it.copy( + card = it.card.copy(batchId = "AB10"), + productType = ProductType.Note, + ) + }, + hasBackupError = false, + ) + } + + private fun createSingleWalletWithToken(): UserWallet { + return UserWallet( + name = "NODL", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ).copy( + productType = ProductType.Note, + walletData = WalletData( + blockchain = "ETH", + token = WalletData.Token( + name = "Ethereum", + symbol = "ETH", + contractAddress = "0x", + decimals = 8, + ), + ), + ), + hasBackupError = false, + ) + } + + private companion object { + + val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + + val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true) + + val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin) + + fun CryptoCurrency.setCanHandleTokens(value: Boolean): CryptoCurrency { + return when (this) { + is CryptoCurrency.Coin -> copy(network = network.copy(canHandleTokens = value)) + is CryptoCurrency.Token -> copy(network = network.copy(canHandleTokens = value)) + } + } + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 84780326f4..f5755416bd 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -6,12 +6,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.getBlockchain import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -26,7 +26,6 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.managetokens.model.* import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -48,12 +47,12 @@ internal class DefaultManageTokensRepository( private val appPreferencesStore: AppPreferencesStore, private val testnetTokensStorage: TestnetTokensStorage, private val excludedBlockchains: ExcludedBlockchains, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, ) : ManageTokensRepository { private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(DemoConfig(), excludedBlockchains) // region getTokenListBatchFlow override fun getTokenListBatchFlow( @@ -77,7 +76,7 @@ internal class DefaultManageTokensRepository( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, isFirstBatchFetching -> - val userWallet = request.params.userWalletId?.let { getUserWallet(it) } + val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict) if (userWallet?.scanResponse?.card?.isTestCard == true) { fetchTestnetCurrencies(userWallet, request) @@ -190,17 +189,11 @@ internal class DefaultManageTokensRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( - currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), isGroupedByNetwork = false, isSortedByBalance = false, ) - private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { - return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } - } - private fun getSupportedBlockchains(userWallet: UserWallet?): List { return userWallet?.scanResponse?.let { it.card.supportedBlockchains(it.cardTypesResolver, excludedBlockchains) @@ -261,7 +254,7 @@ internal class DefaultManageTokensRepository( userWalletId: UserWalletId, sourceNetwork: SourceNetwork, ): CurrencyUnsupportedState? { - val userWallet = getUserWallet(userWalletId = userWalletId) + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) val blockchain = getBlockchain(sourceNetwork.id) return when (sourceNetwork) { is SourceNetwork.Default -> checkTokenUnsupportedState(userWallet = userWallet, blockchain = blockchain) @@ -274,7 +267,7 @@ internal class DefaultManageTokensRepository( rawNetworkId: String, isMainNetwork: Boolean, ): CurrencyUnsupportedState? { - val userWallet = getUserWallet(userWalletId = userWalletId) + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) val blockchain = Blockchain.fromNetworkId(networkId = rawNetworkId) ?: error("Can not create blockchain with given networkId -> $rawNetworkId") return if (isMainNetwork) { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 3f64ae5970..73044971f5 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -1,6 +1,7 @@ package com.tangem.data.managetokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher @@ -32,15 +33,17 @@ internal object ManageTokensDataModule { testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ): ManageTokensRepository { return DefaultManageTokensRepository( - tangemTechApi, - userWalletsStore, - manageTokensUpdateFetcher, - appPreferencesStore, - testnetTokensStorage, - excludedBlockchains, - dispatchers, + tangemTechApi = tangemTechApi, + userWalletsStore = userWalletsStore, + manageTokensUpdateFetcher = manageTokensUpdateFetcher, + appPreferencesStore = appPreferencesStore, + testnetTokensStorage = testnetTokensStorage, + excludedBlockchains = excludedBlockchains, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + dispatchers = dispatchers, ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 6f72de7745..fed32a810e 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -1,27 +1,14 @@ package com.tangem.data.networks.single 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.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.catchOn -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber @@ -30,27 +17,20 @@ import javax.inject.Inject /** * Default implementation of [SingleNetworkStatusFetcher] * - * @param excludedBlockchains excluded blockchains - * @property walletManagersFacade wallet managers facade - * @property networksStatusesStore networks statuses store - * @property userWalletsStore user wallets store - * @property appPreferencesStore app preferences store - * @property dispatchers dispatchers + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( - excludedBlockchains: ExcludedBlockchains, private val walletManagersFacade: WalletManagersFacade, private val networksStatusesStore: NetworksStatusesStoreV2, - private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleNetworkStatusFetcher { - private val demoConfig = DemoConfig() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) - private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val networkStatusFactory = NetworkStatusFactory() override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { @@ -58,8 +38,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) } - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) - val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network) + val networkCurrencies = cardCryptoCurrencyFactory.create( + userWalletId = params.userWalletId, + network = params.network, + ) val result = withContext(dispatchers.io) { walletManagersFacade.update( @@ -92,36 +74,4 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( Timber.e("Failed to fetch network status for $params: $it") networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) } - - private suspend fun createCurrencies(userWallet: UserWallet, network: Network): List { - val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) - - // multi-currency wallet - if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) - - // check if the blockchain of single-currency wallet is the same as network - val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - if (cardBlockchain != blockchain) return emptyList() - - // single-currency wallet with token (NODL) - if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) - } - - // single-currency wallet - return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) - } - - private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { - val response = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), - ) ?: return emptyList() - - return responseCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { - it.networkId == network.backendId && it.derivationPath == network.derivationPath.value - }, - scanResponse = userWallet.scanResponse, - ) - } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index 7f2a925235..a0da0c0c96 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -2,8 +2,8 @@ package com.tangem.data.networks.single import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.walletmanager.WalletManagersFacade @@ -22,39 +22,35 @@ import org.junit.Test */ internal class DefaultSingleNetworkStatusFetcherTest { - private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true) - private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) - private val userWalletsStore: UserWalletsStore = mockk(relaxed = true) + private val walletManagersFacade: WalletManagersFacade = mockk(relaxUnitFun = true) + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val fetcher = DefaultSingleNetworkStatusFetcher( - excludedBlockchains = mockk(relaxed = true), walletManagersFacade = walletManagersFacade, networksStatusesStore = networksStatusesStore, - userWalletsStore = userWalletsStore, - appPreferencesStore = mockk(relaxed = true), + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `fetch network status successfully`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = true, - ) + val params = createParams() + + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result val actual = fetcher(params) coVerifyOrder { - networksStatusesStore.refresh(userWalletId = userWalletId, network = network) - userWalletsStore.getSyncStrict(key = userWalletId) - walletManagersFacade.update(userWalletId, network, emptySet()) + networksStatusesStore.refresh(params.userWalletId, params.network) + cardCryptoCurrencyFactory.create(params.userWalletId, params.network) + walletManagersFacade.update(params.userWalletId, params.network, emptySet()) networksStatusesStore.storeSuccess( - userWalletId = userWalletId, - value = NetworkStatus(network, NetworkStatus.MissedDerivation), + userWalletId = params.userWalletId, + value = NetworkStatus(params.network, NetworkStatus.MissedDerivation), ) } @@ -63,21 +59,17 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status failure`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = true, - ) + val params = createParams() val exception = IllegalStateException() - coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws exception val actual = fetcher(params) coVerifyOrder { - networksStatusesStore.refresh(userWalletId = userWalletId, network = network) - userWalletsStore.getSyncStrict(key = userWalletId) - networksStatusesStore.storeError(userWalletId = userWalletId, network = network) + networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) + networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) } coVerify(inverse = true) { @@ -91,23 +83,21 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status if applyRefresh is false`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = false, - ) + val params = createParams(applyRefresh = false) + + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result val actual = fetcher(params) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWalletId) - walletManagersFacade.update(userWalletId, network, emptySet()) + cardCryptoCurrencyFactory.create(params.userWalletId, params.network) + walletManagersFacade.update(params.userWalletId, params.network, emptySet()) networksStatusesStore.storeSuccess( - userWalletId = userWalletId, - value = NetworkStatus(network, NetworkStatus.MissedDerivation), + userWalletId = params.userWalletId, + value = NetworkStatus(params.network, NetworkStatus.MissedDerivation), ) } @@ -118,8 +108,16 @@ internal class DefaultSingleNetworkStatusFetcherTest { Truth.assertThat(actual.isRight()).isTrue() } + private fun createParams(applyRefresh: Boolean = true): SingleNetworkStatusFetcher.Params { + return SingleNetworkStatusFetcher.Params( + userWalletId = UserWalletId("011"), + network = ethereum.network, + applyRefresh = applyRefresh, + ) + } + private companion object { - val userWalletId = UserWalletId("011") - val network = MockCryptoCurrencyFactory().ethereum.network + + val ethereum = MockCryptoCurrencyFactory().ethereum } } \ 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/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 62f837994d..d0ea5837e7 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -4,7 +4,6 @@ import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConv import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -62,16 +61,6 @@ internal class DefaultNotificationsRepository @Inject constructor( ) } - override suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) = - withContext(dispatchers.io) { - tangemTechApi.associateApplicationIdWithWallets( - applicationId = appId, - body = wallets.map { - WalletIdBody(it) - }, - ).getOrThrow() - } - override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { withContext(dispatchers.io) { tangemTechApi.updatePushTokenForApplicationId( diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 57d8d4c429..691dff0714 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -104,25 +104,6 @@ class DefaultNotificationsRepositoryTest { assertThat(result).isEqualTo(expectedAppId) } - @Test - fun `GIVEN application id and wallet list WHEN associateApplicationIdWithWallets THEN associates them`() = runTest { - // GIVEN - val appId = "test-app-id" - val wallets = listOf("wallet1", "wallet2") - coEvery { - tangemTechApi.associateApplicationIdWithWallets( - appId, - wallets.map { WalletIdBody(it) }, - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.associateApplicationIdWithWallets(appId, wallets) - - // THEN - coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } - } - @Test fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { // GIVEN diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt index 5a5eb2ecfd..1bdef12d69 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt @@ -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 } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt new file mode 100644 index 0000000000..9cd56dacc2 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt @@ -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, + ), + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt new file mode 100644 index 0000000000..fc1799ba0c --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt @@ -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(relaxed = true) + private val appCurrencyResponseStore = mockk(relaxed = true) + private val quotesStore = mockk(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 + 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), + ), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 1172126125..af3cf30f5f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader @@ -33,6 +34,7 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, expressServiceLoader: ExpressServiceLoader, excludedBlockchains: ExcludedBlockchains, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, @@ -43,6 +45,7 @@ internal object TokensDataModule { expressServiceLoader = expressServiceLoader, dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, ) } @@ -74,6 +77,7 @@ internal object TokensDataModule { cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ): NetworksRepository { return DefaultNetworksRepository( networksStatusesStore = networksStatusesStore, @@ -83,6 +87,7 @@ internal object TokensDataModule { cacheRegistry = cacheRegistry, dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 9454dd4a87..d68c468811 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.utils.* import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.* -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.CustomTokensMerger import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.ApiResponseError @@ -51,12 +50,12 @@ internal class DefaultCurrenciesRepository( private val expressServiceLoader: ExpressServiceLoader, private val dispatchers: CoroutineDispatcherProvider, private val excludedBlockchains: ExcludedBlockchains, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) : CurrenciesRepository { private val demoConfig = DemoConfig() private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers) @@ -223,7 +222,7 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh) currency } @@ -237,8 +236,8 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - val currencies = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken( - userWallet.scanResponse, + val currencies = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, ) fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh) currencies @@ -253,7 +252,9 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, + ) .find { it.id == id } requireNotNull(currency) { "Unable to find currency with provided ID: $id" } fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency)) @@ -672,7 +673,7 @@ internal class DefaultCurrenciesRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( - currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), isGroupedByNetwork = false, isSortedByBalance = false, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index b85018c34b..80acea39bf 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -5,8 +5,8 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.network.NetworksStatusesStore @@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network @@ -38,12 +37,11 @@ internal class DefaultNetworksRepository( private val userWalletsStore: UserWalletsStore, private val appPreferencesStore: AppPreferencesStore, private val cacheRegistry: CacheRegistry, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : NetworksRepository { - private val demoConfig = DemoConfig() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val networkStatusFactory = NetworkStatusFactory() @@ -225,10 +223,14 @@ internal class DefaultNetworksRepository( responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, + ) .asSequence() } else { - val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard( + scanResponse = userWallet.scanResponse, + ) sequenceOf(currency) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt deleted file mode 100644 index 1e3ed23c01..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency - -class CardCryptoCurrenciesFactory( - private val demoConfig: DemoConfig, - excludedBlockchains: ExcludedBlockchains, -) { - - private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - - fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { - val card = scanResponse.card - - var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { - demoConfig.demoBlockchains - } else { - listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - } - - if (card.isTestCard) { - blockchains = blockchains.mapNotNull { it.getTestnetVersion() } - } - - return blockchains.mapNotNull { - cryptoCurrencyFactory.createCoin( - blockchain = it, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - } - } - - fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { - val resolver = scanResponse.cardTypesResolver - val blockchain = resolver.getBlockchain() - - val coin = cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - requireNotNull(coin) { "Coin for the single currency card cannot be null" } - - val primaryToken = resolver.getPrimaryToken()?.let { token -> - cryptoCurrencyFactory.createToken( - sdkToken = token, - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - } - - return primaryToken ?: coin - } - - fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { - val resolver = scanResponse.cardTypesResolver - val blockchain = resolver.getBlockchain() - - val coin = cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - requireNotNull(coin) { "Coin for the single currency card cannot be null" } - - val primaryToken = resolver.getPrimaryToken()?.let { token -> - cryptoCurrencyFactory.createToken( - sdkToken = token, - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - } - - return listOfNotNull(coin, primaryToken) - } -} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 6ba07ef2e1..540ffdb8ed 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -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"), ), ), diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index e84f33b3bc..a9ca9af06f 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(deps.arrow.core) /** tests */ + testImplementation(projects.domain.models) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 25bc5ae0b6..1b8c62c423 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,6 +1,8 @@ package com.tangem.data.wallets import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter +import com.tangem.data.wallets.converters.WalletIdBodyConverter +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -18,6 +20,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.wallets.repository.WalletsRepository @@ -37,6 +40,7 @@ internal class DefaultWalletsRepository( private val userWalletsStore: UserWalletsStore, private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, + private val authProvider: AuthProvider, ) : WalletsRepository { override suspend fun shouldSaveUserWalletsSync(): Boolean { @@ -288,6 +292,22 @@ internal class DefaultWalletsRepository( } } + override suspend fun associateWallets(applicationId: String, wallets: List) = + withContext(dispatchers.io) { + val publicKeys = authProvider.getCardsPublicKeys() + val walletsBody = wallets.map { userWallet -> + WalletIdBodyConverter.convert( + userWallet = userWallet, + publicKeys = publicKeys.filterKeys { userWallet.cardsInWallet.contains(it) }, + ) + } + + tangemTechApi.associateApplicationIdWithWallets( + applicationId = applicationId, + body = walletsBody, + ).getOrThrow() + } + private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean { val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow() val isEnabled = walletResponse.notifyStatus diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt b/data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt new file mode 100644 index 0000000000..fdf8d28b3b --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.wallets.converters + +import com.tangem.datasource.api.tangemTech.models.CardInfoBody +import com.tangem.datasource.api.tangemTech.models.WalletIdBody +import com.tangem.domain.wallets.models.UserWallet + +internal object WalletIdBodyConverter { + + fun convert(userWallet: UserWallet, publicKeys: Map): WalletIdBody { + return WalletIdBody( + walletId = userWallet.walletId.stringValue, + name = userWallet.name, + cards = publicKeys.map { + CardInfoBody( + cardId = it.key, + cardPublicKey = it.value, + ) + }, + ) + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 778257f353..a154dc4f99 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.wallets.di import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -26,6 +27,7 @@ internal object WalletsDataModule { tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, + authProvider: AuthProvider, ): WalletsRepository { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, @@ -33,6 +35,7 @@ internal object WalletsDataModule { userWalletsStore = userWalletsStore, seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, + authProvider = authProvider, ) } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index f430dab62f..af1295b684 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -18,6 +18,8 @@ import io.mockk.coVerify import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.api.common.AuthProvider +import com.tangem.domain.wallets.models.UserWallet import org.junit.Before import org.junit.Test @@ -48,6 +50,7 @@ class DefaultWalletsRepositoryTest { userWalletsStore = mockk(), seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, + authProvider = mockk(), ) } @@ -231,4 +234,73 @@ class DefaultWalletsRepositoryTest { coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } coVerify(exactly = 0) { preferencesDataStore.updateData(any()) } } + + @Test + fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest { + // GIVEN + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val wallet2Id = "fedcba0987654321" + val card1PublicKey = "card1_public_key" + val card2PublicKey = "card2_public_key" + + val userWallets = listOf( + mockk { + every { cardsInWallet } returns setOf(card1PublicKey) + every { walletId } returns UserWalletId(wallet1Id) + every { name } returns "Wallet 1" + }, + mockk { + every { cardsInWallet } returns setOf(card2PublicKey) + every { walletId } returns UserWalletId(wallet2Id) + every { name } returns "Wallet 2" + }, + ) + + val publicKeys = mapOf( + card1PublicKey to "public_key_1", + card2PublicKey to "public_key_2", + ) + + val authProvider = mockk { + every { getCardsPublicKeys() } returns publicKeys + } + + repository = DefaultWalletsRepository( + appPreferencesStore = appPreferenceStore, + tangemTechApi = tangemTechApi, + userWalletsStore = mockk(), + seedPhraseNotificationVisibilityStore = mockk(), + dispatchers = dispatchers, + authProvider = authProvider, + ) + + coEvery { + tangemTechApi.associateApplicationIdWithWallets( + eq(applicationId), + any(), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.associateWallets(applicationId, userWallets) + + // THEN + coVerify(exactly = 1) { + tangemTechApi.associateApplicationIdWithWallets( + eq(applicationId), + match { body -> + body.size == 2 && + body.any { + it.walletId == wallet1Id && it.cards.any { card -> card.cardPublicKey == "public_key_1" } && + it.name == "Wallet 1" + } && + body.any { + it.walletId == wallet2Id && it.cards.any { card -> card.cardPublicKey == "public_key_2" } && + it.name == "Wallet 2" + } + }, + ) + } + } } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt new file mode 100644 index 0000000000..2aa327b6d9 --- /dev/null +++ b/data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt @@ -0,0 +1,80 @@ +package com.tangem.data.wallets.converters + +import com.tangem.datasource.api.tangemTech.models.CardInfoBody +import com.tangem.datasource.api.tangemTech.models.WalletIdBody +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.google.common.truth.Truth.assertThat +import io.mockk.mockk +import org.junit.Test + +class WalletIdBodyConverterTest { + + @Test + fun `GIVEN user wallet with cards WHEN convert THEN should return correct WalletIdBody`() { + // GIVEN + val walletId = UserWalletId("1234567890abcdef") + val walletName = "Test Wallet" + val userWallet = UserWallet( + walletId = walletId, + name = walletName, + cardsInWallet = setOf("card1", "card2"), + isMultiCurrency = true, + hasBackupError = false, + scanResponse = mockk(), + ) + val publicKeys = mapOf( + "card1" to "public_key_1", + "card2" to "public_key_2", + ) + + // WHEN + val result = WalletIdBodyConverter.convert(userWallet, publicKeys) + + // THEN + assertThat(result).isEqualTo( + WalletIdBody( + walletId = walletId.stringValue, + name = walletName, + cards = listOf( + CardInfoBody( + cardId = "card1", + cardPublicKey = "public_key_1", + ), + CardInfoBody( + cardId = "card2", + cardPublicKey = "public_key_2", + ), + ), + ), + ) + } + + @Test + fun `GIVEN user wallet without cards WHEN convert THEN should return WalletIdBody with empty cards list`() { + // GIVEN + val walletId = UserWalletId("1234567890abcdef") + val walletName = "Test Wallet" + val userWallet = UserWallet( + walletId = walletId, + name = walletName, + cardsInWallet = emptySet(), + isMultiCurrency = true, + hasBackupError = false, + scanResponse = mockk(), + ) + val publicKeys = emptyMap() + + // WHEN + val result = WalletIdBodyConverter.convert(userWallet, publicKeys) + + // THEN + assertThat(result).isEqualTo( + WalletIdBody( + walletId = walletId.stringValue, + name = walletName, + cards = emptyList(), + ), + ) + } +} \ No newline at end of file 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) diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index d5e99ab745..a8c46854df 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -16,9 +16,6 @@ interface NotificationsRepository { suspend fun incrementTronTokenFeeNotificationShowCounter() - @Throws - suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) - @Throws suspend fun sendPushToken(appId: ApplicationId, pushToken: String) diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt new file mode 100644 index 0000000000..6923e9a50e --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt @@ -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 { + + /** + * Params + * + * @property rawCurrencyId crypto currency id + */ + data class Params( + val rawCurrencyId: CryptoCurrency.RawID, + val appCurrencyId: String?, + ) +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index b1c1e11c7e..37ab17d016 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -27,6 +27,7 @@ class DefaultUserWalletsSyncDelegate( } } + // TODO remove dispatchers whnen UserWalletsListManager will be main safe private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 90768f5e78..eaf24f6cfa 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.wallets.repository import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import kotlinx.coroutines.flow.Flow @@ -54,4 +55,7 @@ interface WalletsRepository { @Throws suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List + + @Throws + suspend fun associateWallets(applicationId: String, wallets: List) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt new file mode 100644 index 0000000000..e67bf5b744 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletsRepository + +class AssociateWalletsWithApplicationIdUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { + val wallets = userWalletsListManager.userWalletsSync + walletsRepository.associateWallets(applicationId.value, wallets) + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9e58bcf67c..ccc406b5ff 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1052" +tangemBlockchainSdk = "develop-1063" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^