Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-12 18:31:31 +03:00
commit 0526a07c37
50 changed files with 1470 additions and 295 deletions

View file

@ -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)
}
}

View file

@ -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<String, String> {
return userWalletsListManager.userWalletsSync.associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
}

View file

@ -11,4 +11,9 @@ interface AuthProvider {
fun getCardPublicKey(): String
fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -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,
)

View file

@ -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<CardInfoBody>,
)

View file

@ -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)
}
}

View file

@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
import androidx.datastore.core.DataStore
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.local.nft.custom.NFTPriceId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
internal class DefaultNFTPersistenceStore(
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore(
}
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTAsset.SalePrice?> = pricesPersistenceStore.data
.map { it[assetId] }
.map { data -> data.associate { it.assetId to it.price }[assetId] }
override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
.data
.firstOrNull()
?.associate { it.assetId to it.price }
override suspend fun saveCollections(collections: List<NFTCollection>) {
collectionsPersistenceStore.updateData {
@ -41,7 +43,7 @@ internal class DefaultNFTPersistenceStore(
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
pricesPersistenceStore.updateData {
it.toMutableMap().apply { this[assetId] = salePrice }
it.toMutableList() + NFTPriceId(assetId = assetId, price = salePrice)
}
}

View file

@ -5,12 +5,11 @@ import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.nft.custom.NFTPriceId
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithCustomKeyTypes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor(
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
defaultValue = emptyMap(),
types = listTypes<NFTPriceId>(),
defaultValue = emptyList(),
),
)
}

View file

@ -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<SDKSalePrice, NFTSalePrice.Value> {
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,
)
}
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
)
}
}

View file

@ -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?
}

View file

@ -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)
}

View file

@ -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<CryptoCurrency>
/**
* Create default coins for multi currency card
*
* @param scanResponse scan response
*/
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin>
/**
* 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<CryptoCurrency>
}

View file

@ -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<CryptoCurrency> {
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<CryptoCurrency.Coin> {
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<CryptoCurrency> {
return with(getSingleWalletCurrencies(scanResponse)) {
listOfNotNull(coin, primaryToken)
}
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
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?)
}

View file

@ -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,
)
}
}

View file

@ -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<CryptoCurrency>()
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<CryptoCurrency>()
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<CryptoCurrency>()
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<CryptoCurrency>()
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))
}
}
}
}

View file

@ -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<Blockchain> {
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) {

View file

@ -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,
)
}

View file

@ -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<CryptoCurrency> {
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<CryptoCurrency> {
val response = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
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,
)
}
}

View file

@ -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
}
}

View file

@ -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<Network, JobHolder>()
private val collectionJobs = ConcurrentHashMap<NFTCollection.Identifier, JobHolder>()
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val nftRuntimeStores = ConcurrentHashMap<String, NFTRuntimeStore>()
private val nftPersistenceStores = ConcurrentHashMap<String, NFTPersistenceStore>()
private val collectionIdConverter = NFTSdkCollectionIdentifierConverter
private val assetIdConverter = NFTSdkAssetIdentifierConverter
override fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>> =
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<Network>,
@ -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,
)
}
}

View file

@ -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<String>) =
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(

View file

@ -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

View file

@ -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
}

View file

@ -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,
),
)
}

View file

@ -0,0 +1,174 @@
package com.tangem.data.quotes.single
import com.google.common.truth.Truth
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher
import com.tangem.data.quotes.store.QuotesStoreV2
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.quotes.single.SingleQuoteFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.math.BigDecimal
internal class DefaultSingleQuoteFetcherTest {
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
private val quotesStore = mockk<QuotesStoreV2>(relaxed = true)
private val multiFetcher = DefaultMultiQuoteFetcher(
tangemTechApi = tangemTechApi,
appCurrencyResponseStore = appCurrencyResponseStore,
quotesStore = quotesStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val singleFetcher = DefaultSingleQuoteFetcher(multiFetcher)
@Test
fun `fetch single quote successfully`() = runTest {
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC"
coEvery {
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId))
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.storeActual(values = successResponse.quotes)
}
coVerify(inverse = true) {
quotesStore.storeError(currenciesIds = any())
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest {
val appCurrencyId = "usd"
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
val coinIds = "BTC"
coEvery {
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.storeActual(values = successResponse.quotes)
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest {
val appCurrencyId = ""
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
quotesStore.storeError(currenciesIds = setOf(currenciesId))
}
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
.isEqualTo("Unable to get AppCurrency for updating quotes")
}
@Test
fun `fetch single quote failure because api request failed`() = runTest {
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC"
@Suppress("UNCHECKED_CAST")
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.storeError(currenciesIds = setOf(currenciesId))
}
coVerify(inverse = true) {
quotesStore.storeActual(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
@Test
fun `fetch single quote failure because app currency not found`() = runTest {
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
appCurrencyResponseStore.getSyncOrNull()
quotesStore.storeError(currenciesIds = setOf(currenciesId))
}
coVerify(inverse = true) {
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
quotesStore.storeActual(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
private companion object {
val currenciesId = CryptoCurrency.RawID(value = "BTC")
val usdAppCurrency = CurrenciesResponse.Currency(
id = "USD".lowercase(),
code = "USD",
name = "US Dollar",
unit = "$",
type = "fiat",
rateBTC = "",
)
val successResponse = QuotesResponse(
quotes = mapOf(
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
),
)
}
}

View file

@ -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,
)
}

View file

@ -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,
)

View file

@ -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)
}

View file

@ -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<CryptoCurrency.Coin> {
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<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 listOfNotNull(coin, primaryToken)
}
}

View file

@ -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"),
),
),

View file

@ -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)

View file

@ -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<SeedPhraseNotificationsStatuses>,
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<UserWallet>) =
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

View file

@ -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<String, String>): WalletIdBody {
return WalletIdBody(
walletId = userWallet.walletId.stringValue,
name = userWallet.name,
cards = publicKeys.map {
CardInfoBody(
cardId = it.key,
cardPublicKey = it.value,
)
},
)
}
}

View file

@ -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,
)
}

View file

@ -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<UserWallet> {
every { cardsInWallet } returns setOf(card1PublicKey)
every { walletId } returns UserWalletId(wallet1Id)
every { name } returns "Wallet 1"
},
mockk<UserWallet> {
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<AuthProvider> {
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"
}
},
)
}
}
}

View file

@ -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<String, String>()
// WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
// THEN
assertThat(result).isEqualTo(
WalletIdBody(
walletId = walletId.stringValue,
name = walletName,
cards = emptyList(),
),
)
}
}

View file

@ -22,4 +22,5 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.quotes)
}

View file

@ -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()
}

View file

@ -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<Throwable, Unit> {
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,
),
)
}
}
}

View file

@ -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<Throwable, Flow<NFTSalePrice>> {
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))
}
}
}
}
}

View file

@ -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<Network>): Flow<List<NFTCollections>>
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<Network>)
suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier)

View file

@ -16,9 +16,6 @@ interface NotificationsRepository {
suspend fun incrementTronTokenFeeNotificationShowCounter()
@Throws
suspend fun associateApplicationIdWithWallets(appId: String, wallets: List<String>)
@Throws
suspend fun sendPushToken(appId: ApplicationId, pushToken: String)

View file

@ -0,0 +1,22 @@
package com.tangem.domain.quotes.single
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Fetcher of quote for [CryptoCurrency.RawID]
*
[REDACTED_AUTHOR]
*/
interface SingleQuoteFetcher : FlowFetcher<SingleQuoteFetcher.Params> {
/**
* Params
*
* @property rawCurrencyId crypto currency id
*/
data class Params(
val rawCurrencyId: CryptoCurrency.RawID,
val appCurrencyId: String?,
)
}

View file

@ -27,6 +27,7 @@ class DefaultUserWalletsSyncDelegate(
}
}
// TODO remove dispatchers whnen UserWalletsListManager will be main safe
private suspend fun renameUserWallet(
userWalletId: UserWalletId,
name: String,

View file

@ -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<UserWalletRemoteInfo>
@Throws
suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>)
}

View file

@ -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<Throwable, Unit> = Either.catch {
val wallets = userWalletsListManager.userWalletsSync
walletsRepository.associateWallets(applicationId.value, wallets)
}
}

View file

@ -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 ^