Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-19 11:07:06 +04:00
parent c613890b9a
commit e96b780d21
10 changed files with 577 additions and 272 deletions

View file

@ -2,17 +2,40 @@ package com.tangem.datasource.local.network.entity
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount
import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal import java.math.BigDecimal
/**
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
*
* @see [com.tangem.domain.tokens.model.NetworkStatus]
*/
sealed interface NetworkStatusDM { sealed interface NetworkStatusDM {
/** Network id */
val networkId: Network.ID val networkId: Network.ID
/** Derivation path */
val derivationPath: DerivationPath val derivationPath: DerivationPath
/** Selected address */
val selectedAddress: String val selectedAddress: String
/** Available address */
val availableAddresses: Set<Address> val availableAddresses: Set<Address>
/**
* Verified
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
*/
@NameLabel("amounts") @NameLabel("amounts")
data class Verified( data class Verified(
@Json(name = "network_id") override val networkId: Network.ID, @Json(name = "network_id") override val networkId: Network.ID,
@ -22,6 +45,16 @@ sealed interface NetworkStatusDM {
@Json(name = "amounts") val amounts: Map<String, BigDecimal>, @Json(name = "amounts") val amounts: Map<String, BigDecimal>,
) : NetworkStatusDM ) : NetworkStatusDM
/**
* No account
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amountToCreateAccount amount to create account
* @property errorMessage error message
*/
@NameLabel("amount_to_create_account") @NameLabel("amount_to_create_account")
data class NoAccount( data class NoAccount(
@Json(name = "network_id") override val networkId: Network.ID, @Json(name = "network_id") override val networkId: Network.ID,

View file

@ -2,26 +2,15 @@ package com.tangem.data.networks.multi
import arrow.core.raise.catch import arrow.core.raise.catch
import arrow.core.raise.ensure import arrow.core.raise.ensure
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.setSourceAsOnlyCache import com.tangem.data.networks.store.setSourceAsOnlyCache
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.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
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.wallets.models.UserWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -31,28 +20,24 @@ import javax.inject.Inject
/** /**
* Default implementation of [MultiNetworkStatusFetcher] * Default implementation of [MultiNetworkStatusFetcher]
* *
* @property singleNetworkStatusFetcher single network status fetcher
* @property networksStatusesStore networks statuses store * @property networksStatusesStore networks statuses store
* @property userWalletsStore user wallets store
* @property cardCryptoCurrencyFactory card crypto currency factory
* @property commonNetworkStatusFetcher common network status fetcher
* @property dispatchers dispatchers
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal class DefaultMultiNetworkStatusFetcher @Inject constructor( internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
excludedBlockchains: ExcludedBlockchains,
private val networksStatusesStore: NetworksStatusesStoreV2, private val networksStatusesStore: NetworksStatusesStoreV2,
private val userWalletsStore: UserWalletsStore, private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiNetworkStatusFetcher { ) : MultiNetworkStatusFetcher {
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) }
override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) {
// Optimization!
// Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store.
// So if we update all networks at once, it will be more efficient.
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks) networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks)
val userWallet = catch( val userWallet = catch(
@ -67,27 +52,39 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
}, },
) )
val isNotSingleWallet = with(userWallet.scanResponse.cardTypesResolver) { val cardTypesResolver = userWallet.cardTypesResolver
val isWalletSupported = with(cardTypesResolver) {
isMultiwalletAllowed() || isSingleWalletWithToken() isMultiwalletAllowed() || isSingleWalletWithToken()
} }
ensure(isNotSingleWallet) { ensure(isWalletSupported) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, networks = params.networks) networksStatusesStore.setSourceAsOnlyCache(
userWalletId = params.userWalletId,
networks = params.networks,
)
IllegalStateException("User wallet is not multi-currency") IllegalStateException("User wallet is not multi-currency")
} }
val networksCurrencies = createCurrencies(userWallet = userWallet, networks = params.networks) val networksCurrencies = if (cardTypesResolver.isMultiwalletAllowed()) {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(
userWallet = userWallet,
networks = params.networks,
)
} else {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.scanResponse,
)
.groupBy { it.network }
}
val result = coroutineScope { val result = coroutineScope {
params.networks params.networks
.map { network -> .map { network ->
async { async {
singleNetworkStatusFetcher( commonNetworkStatusFetcher.fetch(
params = SingleNetworkStatusFetcher.Params.Prepared( userWalletId = params.userWalletId,
userWalletId = params.userWalletId, network = network,
network = network, networkCurrencies = networksCurrencies[network].orEmpty().toSet(),
addedNetworkCurrencies = networksCurrencies.filter { it.network == network }.toSet(),
),
) )
} }
} }
@ -100,31 +97,4 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
IllegalStateException("Failed to fetch network statuses") IllegalStateException("Failed to fetch network statuses")
} }
} }
private suspend fun createCurrencies(userWallet: UserWallet, networks: Set<Network>): Set<CryptoCurrency> {
val blockchains = networks.map { Blockchain.fromNetworkId(networkId = it.backendId) }
// multi-currency wallet
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, networks = networks)
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
if (!blockchains.contains(cardBlockchain)) return emptySet()
return cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet()
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set<Network>): Set<CryptoCurrency> {
val response = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
) ?: return emptySet()
return responseCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
scanResponse = userWallet.scanResponse,
)
.toSet()
}
} }

View file

@ -2,70 +2,46 @@ package com.tangem.data.networks.single
import arrow.core.Either import arrow.core.Either
import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.data.networks.store.storeStatus
import com.tangem.data.networks.utils.NetworkStatusFactory
import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
* Default implementation of [SingleNetworkStatusFetcher] * Default implementation of [SingleNetworkStatusFetcher]
* *
* @property walletManagersFacade wallet managers facade * @property commonNetworkStatusFetcher common network status fetcher
* @property networksStatusesStore networks statuses store * @property networksStatusesStore networks statuses store
* @property cardCryptoCurrencyFactory card crypto currency factory * @property cardCryptoCurrencyFactory card crypto currency factory
* @property dispatchers dispatchers * @property dispatchers dispatchers
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class DefaultSingleNetworkStatusFetcher @Inject constructor( internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
private val walletManagersFacade: WalletManagersFacade, private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
private val networksStatusesStore: NetworksStatusesStoreV2, private val networksStatusesStore: NetworksStatusesStoreV2,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : SingleNetworkStatusFetcher { ) : SingleNetworkStatusFetcher {
override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either<Throwable, Unit> {
val networkCurrencies = when (params) { return Either.catchOn(dispatchers.default) {
is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
is SingleNetworkStatusFetcher.Params.Simple -> {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
cardCryptoCurrencyFactory.create( val networkCurrencies = cardCryptoCurrencyFactory.create(
userWalletId = params.userWalletId,
network = params.network,
)
}
}
val result = withContext(dispatchers.io) {
walletManagersFacade.update(
userWalletId = params.userWalletId, userWalletId = params.userWalletId,
network = params.network, network = params.network,
extraTokens = networkCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.toSet(),
) )
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = networkCurrencies.toSet(),
)
.onLeft { throw it }
} }
val status = NetworkStatusFactory.create(
network = params.network,
updatingResult = result,
addedCurrencies = networkCurrencies.toSet(),
)
networksStatusesStore.storeStatus(userWalletId = params.userWalletId, status = status)
} }
.onLeft {
Timber.e("Failed to fetch network status for $params: $it")
networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network)
}
} }

View file

@ -1,119 +1,409 @@
package com.tangem.data.networks.multi package com.tangem.data.networks.multi
import arrow.core.Either
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
// internal class DefaultMultiNetworkStatusFetcherTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS)
// internal class DefaultMultiNetworkStatusFetcherTest {
// private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk()
// private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true)
// private val userWalletsStore: UserWalletsStore = mockk()
// private val fetcher = DefaultMultiNetworkStatusFetcher( private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
// singleNetworkStatusFetcher = singleNetworkStatusFetcher, private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
// networksStatusesStore = networksStatusesStore,
// ) private val fetcher = DefaultMultiNetworkStatusFetcher(
// networksStatusesStore = networksStatusesStore,
// @Test userWalletsStore = userWalletsStore,
// fun `fetch networks statuses successfully`() = runTest { cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) commonNetworkStatusFetcher = commonNetworkStatusFetcher,
// dispatchers = TestingCoroutineDispatcherProvider(),
// val ethParams = SingleNetworkStatusFetcher.Params( )
// userWalletId = userWalletId,
// network = ethereumAndStellar.first(), @BeforeEach
// applyRefresh = false, fun resetMocks() {
// ) clearMocks(networksStatusesStore, userWalletsStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher)
// }
// val stellarParams = SingleNetworkStatusFetcher.Params(
// userWalletId = userWalletId, @Test
// network = ethereumAndStellar.last(), fun `fetch successfully for multi-currency card`() = runTest {
// applyRefresh = false, // Arrange
// ) val networks = setOf(ethereum.network, cardano.network)
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
// coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() val userWallet = MockUserWalletFactory.create()
// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() val cardTypesResolver = mockk<CardTypesResolver>()
// val networksCurrencies = mapOf(
// val actual = fetcher(params) ethereum.network to listOf(ethereum),
// cardano.network to listOf(cardano),
// coVerify { )
// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) val ethereumFetcherResult = Either.Right(Unit)
// singleNetworkStatusFetcher(ethParams) val cardanoFetcherResult = Either.Right(Unit)
// singleNetworkStatusFetcher(stellarParams)
// } coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
// every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
// Truth.assertThat(actual.isRight()).isTrue()
// } mockkStatic(UserWallet::cardTypesResolver)
// every { userWallet.cardTypesResolver } returns cardTypesResolver
// @Test coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true
// fun `fetch networks statuses failure if one of them fails`() = runTest {
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) coEvery {
// cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
// val ethParams = SingleNetworkStatusFetcher.Params( } returns networksCurrencies
// userWalletId = userWalletId,
// network = ethereumAndStellar.first(), coEvery {
// applyRefresh = false, commonNetworkStatusFetcher.fetch(
// ) userWalletId = params.userWalletId,
// network = ethereum.network,
// val stellarParams = SingleNetworkStatusFetcher.Params( networkCurrencies = setOf(ethereum),
// userWalletId = userWalletId, )
// network = ethereumAndStellar.last(), } returns ethereumFetcherResult
// applyRefresh = false,
// ) coEvery {
// commonNetworkStatusFetcher.fetch(
// val ethException = IllegalStateException("eth") userWalletId = params.userWalletId,
// coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() network = cardano.network,
// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() networkCurrencies = setOf(cardano),
// )
// val actual = fetcher(params) } returns cardanoFetcherResult
//
// coVerify { // Act
// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) val actual = fetcher(params)
// singleNetworkStatusFetcher(ethParams)
// singleNetworkStatusFetcher(stellarParams) // Assert
// } val expected = Either.Right(Unit)
//
// val expected = IllegalStateException("Failed to fetch network statuses") Truth.assertThat(actual).isEqualTo(expected)
//
// Truth.assertThat(actual.isLeft()).isTrue() coVerifyOrder {
// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) userWalletsStore.getSyncStrict(key = userWalletId)
// } cardTypesResolver.isMultiwalletAllowed()
// cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
// @Test commonNetworkStatusFetcher.fetch(
// fun `fetch networks statuses failure if all of them fails`() = runTest { userWalletId = params.userWalletId,
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) network = ethereum.network,
// networkCurrencies = setOf(ethereum),
// val ethParams = SingleNetworkStatusFetcher.Params( )
// userWalletId = userWalletId, commonNetworkStatusFetcher.fetch(
// network = ethereumAndStellar.first(), userWalletId = params.userWalletId,
// applyRefresh = false, network = cardano.network,
// ) networkCurrencies = setOf(cardano),
// )
// val stellarParams = SingleNetworkStatusFetcher.Params( }
// userWalletId = userWalletId,
// network = ethereumAndStellar.last(), coVerify(inverse = true) {
// applyRefresh = false, networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
// ) cardTypesResolver.isSingleWalletWithToken()
// cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
// coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() }
// coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() }
//
// val actual = fetcher(params) @Test
// fun `fetch successfully for single-currency card with token`() = runTest {
// coVerify { // Arrange
// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) val networks = setOf(ethereum.network)
// singleNetworkStatusFetcher(ethParams) val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
// singleNetworkStatusFetcher(stellarParams) val userWallet = MockUserWalletFactory.create()
// } val cardTypesResolver = mockk<CardTypesResolver>()
// val networksCurrencies = listOf(ethereum)
// val expected = IllegalStateException("Failed to fetch network statuses") val ethereumFetcherResult = Either.Right(Unit)
//
// Truth.assertThat(actual.isLeft()).isTrue() coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
// } mockkStatic(UserWallet::cardTypesResolver)
// every { userWallet.cardTypesResolver } returns cardTypesResolver
// private companion object { coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false
// val userWalletId = UserWalletId("011") coEvery { cardTypesResolver.isSingleWalletWithToken() } returns true
// val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet()
// } coEvery {
// } cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
} returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
} returns ethereumFetcherResult
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Right(Unit)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any())
}
}
@Test
fun `fetch failure if one of them fails`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
val networksCurrencies = mapOf(
ethereum.network to listOf(ethereum),
cardano.network to listOf(cardano),
)
val ethereumFetcherResult = Either.Left(IllegalStateException())
val cardanoFetcherResult = Either.Right(Unit)
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true
coEvery {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
} returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
} returns ethereumFetcherResult
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
)
} returns cardanoFetcherResult
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(IllegalStateException("Failed to fetch network statuses"))
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
}
}
@Test
fun `fetch failure if all of them fails`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
val networksCurrencies = mapOf(
ethereum.network to listOf(ethereum),
cardano.network to listOf(cardano),
)
val ethereumFetcherResult = Either.Left(IllegalStateException("ethereum"))
val cardanoFetcherResult = Either.Left(IllegalStateException("cardano"))
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true
coEvery {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
} returns networksCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
} returns ethereumFetcherResult
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
)
} returns cardanoFetcherResult
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(IllegalStateException("Failed to fetch network statuses"))
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardTypesResolver.isSingleWalletWithToken()
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
}
}
@Test
fun `fetch failure if userWalletsStore throws exception`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWalletStoreException = IllegalStateException()
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } throws userWalletStoreException
coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(userWalletStoreException)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
}
coVerify(inverse = true) {
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any())
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
commonNetworkStatusFetcher.fetch(any(), any(), any())
}
}
@Test
fun `fetch failure if card is single-currency`() = runTest {
// Arrange
val networks = setOf(ethereum.network, cardano.network)
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks)
val userWallet = MockUserWalletFactory.create()
val cardTypesResolver = mockk<CardTypesResolver>()
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit
every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet
mockkStatic(UserWallet::cardTypesResolver)
every { userWallet.cardTypesResolver } returns cardTypesResolver
coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false
coEvery { cardTypesResolver.isSingleWalletWithToken() } returns false
coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(IllegalStateException("User wallet is not multi-currency"))
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks)
userWalletsStore.getSyncStrict(key = userWalletId)
cardTypesResolver.isMultiwalletAllowed()
cardTypesResolver.isSingleWalletWithToken()
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
}
coVerify(inverse = true) {
networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any())
cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any())
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any())
commonNetworkStatusFetcher.fetch(any(), any(), any())
}
}
private companion object {
val userWalletId = UserWalletId("011")
val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
val ethereum = cryptoCurrencyFactory.ethereum
val cardano = cryptoCurrencyFactory.cardano
}
}

View file

@ -1,97 +1,141 @@
package com.tangem.data.networks.single package com.tangem.data.networks.single
import arrow.core.Either
import arrow.core.left
import com.google.common.truth.Truth import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.storeSuccess
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery import io.mockk.*
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultSingleNetworkStatusFetcherTest { internal class DefaultSingleNetworkStatusFetcherTest {
private val walletManagersFacade: WalletManagersFacade = mockk(relaxUnitFun = true) private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true)
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val fetcher = DefaultSingleNetworkStatusFetcher( private val fetcher = DefaultSingleNetworkStatusFetcher(
walletManagersFacade = walletManagersFacade, commonNetworkStatusFetcher = commonNetworkStatusFetcher,
networksStatusesStore = networksStatusesStore, networksStatusesStore = networksStatusesStore,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
dispatchers = TestingCoroutineDispatcherProvider(), dispatchers = TestingCoroutineDispatcherProvider(),
) )
@BeforeEach
fun resetMocks() {
clearMocks(commonNetworkStatusFetcher, networksStatusesStore, cardCryptoCurrencyFactory)
}
@Test @Test
fun `fetch network status successfully`() = runTest { fun `fetch successfully`() = runTest {
val params = createParams() // Arrange
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network)
val networkCurrencies = listOf(ethereum)
val commonFetcherResult = Either.Right(Unit)
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies
val result = UpdateWalletManagerResult.MissedDerivation coEvery {
coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = networkCurrencies.toSet(),
)
} returns commonFetcherResult
// Act
val actual = fetcher(params) val actual = fetcher(params)
// Assert
val expected = commonFetcherResult
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { coVerifyOrder {
networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) networksStatusesStore.setSourceAsCache(params.userWalletId, params.network)
cardCryptoCurrencyFactory.create(params.userWalletId, params.network) cardCryptoCurrencyFactory.create(params.userWalletId, params.network)
walletManagersFacade.update(params.userWalletId, params.network, emptySet()) commonNetworkStatusFetcher.fetch(params.userWalletId, params.network, setOf(ethereum))
networksStatusesStore.storeSuccess(
userWalletId = params.userWalletId,
status = NetworkStatus(params.network, NetworkStatus.MissedDerivation),
)
} }
Truth.assertThat(actual.isRight()).isTrue()
} }
@Test @Test
fun `fetch network status failure`() = runTest { fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest {
val params = createParams() // Arrange
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network)
val factoryException = IllegalStateException()
val exception = IllegalStateException() coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws exception coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws factoryException
// Act
val actual = fetcher(params) val actual = fetcher(params)
// Arrange
val expected = Either.Left(factoryException)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { coVerifyOrder {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network)
// networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network)
} }
coVerify(inverse = true) { coVerify(inverse = true) {
walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any()) commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any())
// networksStatusesStore.storeSuccess(userWalletId = any(), status = any())
} }
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isEqualTo(exception)
} }
private fun createParams(): SingleNetworkStatusFetcher.Params { @Test
return SingleNetworkStatusFetcher.Params.Simple( fun `fetch failure if commonNetworkStatusFetcher returns exception`() = runTest {
userWalletId = UserWalletId("011"), // Arrange
network = ethereum.network, val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network)
) val networkCurrencies = listOf(ethereum)
val commonFetcherResult = IllegalStateException().left()
coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = networkCurrencies.toSet(),
)
} returns commonFetcherResult
// Act
val actual = fetcher(params)
// Arrange
val expected = commonFetcherResult
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network)
cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = params.network,
networkCurrencies = setOf(ethereum),
)
}
} }
private companion object { private companion object {
val userWalletId = UserWalletId("011")
val ethereum = MockCryptoCurrencyFactory().ethereum val ethereum = MockCryptoCurrencyFactory().ethereum
} }
} }

View file

@ -5,7 +5,7 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
/** /**
* Fetcher of network status [Network] for wallet with [UserWalletId] * Fetcher of network status [Network] for multi-currency wallet with [UserWalletId]
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */

View file

@ -1,7 +1,6 @@
package com.tangem.domain.networks.single package com.tangem.domain.networks.single
import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
@ -12,18 +11,11 @@ import com.tangem.domain.wallets.models.UserWalletId
*/ */
interface SingleNetworkStatusFetcher : FlowFetcher<SingleNetworkStatusFetcher.Params> { interface SingleNetworkStatusFetcher : FlowFetcher<SingleNetworkStatusFetcher.Params> {
/** Params */ /**
sealed interface Params { * Params
*
val userWalletId: UserWalletId * @property userWalletId user wallet id
val network: Network * @property network network
*/
data class Simple(override val userWalletId: UserWalletId, override val network: Network) : Params data class Params(val userWalletId: UserWalletId, val network: Network)
data class Prepared(
override val userWalletId: UserWalletId,
override val network: Network,
val addedNetworkCurrencies: Set<CryptoCurrency>,
) : Params
}
} }

View file

@ -119,7 +119,7 @@ class FetchCurrencyStatusUseCase(
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
singleNetworkStatusFetcher( singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network),
) )
.mapLeft { CurrencyStatusError.DataError(it) } .mapLeft { CurrencyStatusError.DataError(it) }
.bind() .bind()

View file

@ -38,7 +38,7 @@ class UpdateDelayedNetworkStatusUseCase(
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
singleNetworkStatusFetcher( singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network),
) )
.mapLeft(CurrencyStatusError::DataError) .mapLeft(CurrencyStatusError::DataError)
.bind() .bind()

View file

@ -82,7 +82,7 @@ class SendTransactionUseCase(
return sendResult return sendResult
.onRight { .onRight {
singleNetworkStatusFetcher( singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple( params = SingleNetworkStatusFetcher.Params(
userWalletId = userWallet.walletId, userWalletId = userWallet.walletId,
network = network, network = network,
), ),